{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/usestring-ai/json-schema/usestring-ai-build-schema.json",
  "title": "Build",
  "description": "A build, as the customer sees it. Some fields the core holds are deliberately not projected — a feeds-repo pull request URL and the brain's tool arguments — because they are operator detail rather than customer copy.",
  "x-generated": "2026-10-31",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/feedbuilder-usestring-ai-openapi.yaml#/components/schemas/Build",
  "properties": {
    "activity": {
      "items": {
        "$ref": "#/$defs/Activity"
      },
      "type": "array"
    },
    "auto_proceed": {
      "description": "Whether the build advances past gates that can be auto-answered. Change it with PATCH /v1/builds/{id}.",
      "type": "boolean"
    },
    "completed_at": {
      "format": "date-time",
      "type": "string"
    },
    "created_at": {
      "format": "date-time",
      "type": "string"
    },
    "error": {
      "description": "Requester-facing failure copy — the same string the run-finished email carries. Not an operator diagnostic.",
      "type": "string"
    },
    "gate_history": {
      "items": {
        "$ref": "#/$defs/GateRecord"
      },
      "type": "array"
    },
    "id": {
      "type": "string"
    },
    "messages": {
      "items": {
        "$ref": "#/$defs/InputRecord"
      },
      "type": "array"
    },
    "object": {
      "type": "string"
    },
    "pending_gate": {
      "$ref": "#/$defs/Gate"
    },
    "progress": {
      "$ref": "#/$defs/Progress"
    },
    "query": {
      "type": "string"
    },
    "report": {
      "$ref": "#/$defs/Report"
    },
    "status": {
      "description": "One of `running`, `completed`, `errored`, `cancelled`, `awaiting_input`, `unknown`. `awaiting_input` means the build is parked on a gate waiting for you, and it is exactly equivalent to `pending_gate` being present — the two never disagree, so either one answers \"is this waiting on me?\". A terminal build never reports a pending gate, even if it was holding one when it ended.",
      "type": "string"
    },
    "updated_at": {
      "format": "date-time",
      "type": "string"
    },
    "version": {
      "format": "int64",
      "minimum": 0,
      "type": "integer"
    }
  },
  "required": [
    "id",
    "object",
    "version",
    "status",
    "query",
    "auto_proceed"
  ],
  "type": "object",
  "$defs": {
    "Activity": {
      "properties": {
        "at": {
          "format": "date-time",
          "type": "string"
        },
        "text": {
          "type": "string"
        },
        "tool_name": {
          "type": "string"
        },
        "type": {
          "type": "string"
        }
      },
      "required": [
        "type"
      ],
      "type": "object"
    },
    "Choice": {
      "properties": {
        "description": {
          "type": "string"
        },
        "id": {
          "type": "string"
        },
        "label": {
          "type": "string"
        }
      },
      "required": [
        "id"
      ],
      "type": "object"
    },
    "ChoiceOptions": {
      "properties": {
        "choices": {
          "items": {
            "$ref": "#/$defs/Choice"
          },
          "type": "array"
        }
      },
      "required": [
        "choices"
      ],
      "type": "object"
    },
    "Field": {
      "properties": {
        "description": {
          "type": "string"
        },
        "key": {
          "type": "string"
        },
        "repeated": {
          "type": "boolean"
        },
        "required": {
          "type": "boolean"
        },
        "type": {
          "type": "string"
        }
      },
      "required": [
        "key",
        "type",
        "repeated",
        "required"
      ],
      "type": "object"
    },
    "Gate": {
      "properties": {
        "expires_at": {
          "format": "date-time",
          "type": "string"
        },
        "id": {
          "type": "string"
        },
        "kind": {
          "description": "Which of the eight gate kinds this is: `choice`, `confirm`, `schema_review`, `text`, `source_picker`, `publish_review`, `initial_query`, `questionnaire`. Read off the options payload, so the kind and the options can never disagree. NEW KINDS APPEAR WITHOUT A VERSION BUMP — treat an unrecognised kind as answerable with a plain `{\"text\": \"...\"}`.",
          "type": "string"
        },
        "object": {
          "type": "string"
        },
        "options": {
          "description": "The gate's payload. Its shape follows `kind`: `choice` carries ChoiceOptions, `schema_review` SchemaReviewOptions, `source_picker` SourcePickerOptions, `publish_review` PublishReviewOptions, `initial_query` InitialQueryOptions, `questionnaire` QuestionnaireOptions, and `confirm` and `text` carry an empty object. A kind you do not recognise is still answerable with a plain {\"text\": \"...\"}.",
          "oneOf": [
            {
              "$ref": "#/$defs/ChoiceOptions"
            },
            {
              "$ref": "#/$defs/SchemaReviewOptions"
            },
            {
              "$ref": "#/$defs/SourcePickerOptions"
            },
            {
              "$ref": "#/$defs/PublishReviewOptions"
            },
            {
              "$ref": "#/$defs/InitialQueryOptions"
            },
            {
              "$ref": "#/$defs/QuestionnaireOptions"
            },
            {
              "additionalProperties": false,
              "description": "`confirm` and `text` gates carry no options.",
              "title": "EmptyOptions",
              "type": "object"
            }
          ]
        },
        "prompt": {
          "type": "string"
        },
        "raised_at": {
          "format": "date-time",
          "type": "string"
        },
        "suggested": {
          "$ref": "#/$defs/GateAnswer"
        }
      },
      "required": [
        "id",
        "object",
        "kind",
        "prompt"
      ],
      "type": "object"
    },
    "GateAnswer": {
      "description": "One gate resolution. EXACTLY ONE member may be set, and which member is set picks the answer kind — there is no `kind` property, deliberately: a discriminator that can disagree with its payload is a second thing to validate and a way for a caller to be coerced onto an arm they did not fill in. Setting more than one member is a 400 (`gate_answer_ambiguous`); setting none is a 400 (`gate_answer_required`).\n\n`text` IS THE ESCAPE HATCH: a plain {\"text\": \"...\"} is a valid answer to EVERY gate kind, including kinds that did not exist when your client was generated. The brain raises kinds dynamically and new ones appear without a version bump, so a client that meets a `kind` it has no renderer for should show the gate's `prompt` and send the operator's typed reply as `text` rather than failing the build.\n\nAn answer whose member does not match the gate's kind is refused by the core with a 400. This API does not know the gate's kind and never guesses one.",
      "oneOf": [
        {
          "additionalProperties": false,
          "properties": {
            "choice_id": {
              "type": "string"
            }
          },
          "required": [
            "choice_id"
          ],
          "title": "choice_id",
          "type": "object"
        },
        {
          "additionalProperties": false,
          "properties": {
            "confirm": {
              "type": "boolean"
            }
          },
          "required": [
            "confirm"
          ],
          "title": "confirm",
          "type": "object"
        },
        {
          "additionalProperties": false,
          "properties": {
            "schema": {
              "$ref": "#/$defs/SchemaReviewAnswer"
            }
          },
          "required": [
            "schema"
          ],
          "title": "schema",
          "type": "object"
        },
        {
          "additionalProperties": false,
          "properties": {
            "text": {
              "description": "A free-text answer. Valid against EVERY gate kind — this is the escape hatch for kinds your client does not recognise.",
              "type": "string"
            }
          },
          "required": [
            "text"
          ],
          "title": "text",
          "type": "object"
        },
        {
          "additionalProperties": false,
          "properties": {
            "source": {
              "$ref": "#/$defs/SourcePickerAnswer"
            }
          },
          "required": [
            "source"
          ],
          "title": "source",
          "type": "object"
        },
        {
          "additionalProperties": false,
          "properties": {
            "publish": {
              "$ref": "#/$defs/PublishReviewAnswer"
            }
          },
          "required": [
            "publish"
          ],
          "title": "publish",
          "type": "object"
        },
        {
          "additionalProperties": false,
          "properties": {
            "initial_query": {
              "type": "string"
            }
          },
          "required": [
            "initial_query"
          ],
          "title": "initial_query",
          "type": "object"
        },
        {
          "additionalProperties": false,
          "properties": {
            "questionnaire": {
              "$ref": "#/$defs/QuestionnaireAnswer"
            }
          },
          "required": [
            "questionnaire"
          ],
          "title": "questionnaire",
          "type": "object"
        }
      ]
    },
    "GateRecord": {
      "properties": {
        "answer": {
          "$ref": "#/$defs/GateAnswer"
        },
        "answered_at": {
          "format": "date-time",
          "type": "string"
        },
        "id": {
          "type": "string"
        },
        "kind": {
          "description": "Which of the eight gate kinds this was. See `Gate.kind`.",
          "type": "string"
        },
        "options": {
          "description": "The gate's payload. Its shape follows `kind`: `choice` carries ChoiceOptions, `schema_review` SchemaReviewOptions, `source_picker` SourcePickerOptions, `publish_review` PublishReviewOptions, `initial_query` InitialQueryOptions, `questionnaire` QuestionnaireOptions, and `confirm` and `text` carry an empty object. A kind you do not recognise is still answerable with a plain {\"text\": \"...\"}.",
          "oneOf": [
            {
              "$ref": "#/$defs/ChoiceOptions"
            },
            {
              "$ref": "#/$defs/SchemaReviewOptions"
            },
            {
              "$ref": "#/$defs/SourcePickerOptions"
            },
            {
              "$ref": "#/$defs/PublishReviewOptions"
            },
            {
              "$ref": "#/$defs/InitialQueryOptions"
            },
            {
              "$ref": "#/$defs/QuestionnaireOptions"
            },
            {
              "additionalProperties": false,
              "description": "`confirm` and `text` gates carry no options.",
              "title": "EmptyOptions",
              "type": "object"
            }
          ]
        },
        "origin": {
          "type": "string"
        },
        "prompt": {
          "type": "string"
        },
        "raised_at": {
          "format": "date-time",
          "type": "string"
        }
      },
      "required": [
        "id",
        "kind",
        "prompt",
        "origin"
      ],
      "type": "object"
    },
    "InitialQueryOptions": {
      "properties": {
        "refined_request": {
          "type": "string"
        },
        "source_url": {
          "type": "string"
        }
      },
      "required": [
        "refined_request"
      ],
      "type": "object"
    },
    "InputRecord": {
      "properties": {
        "interrupt": {
          "type": "boolean"
        },
        "sent_at": {
          "format": "date-time",
          "type": "string"
        },
        "text": {
          "type": "string"
        }
      },
      "required": [
        "text",
        "interrupt"
      ],
      "type": "object"
    },
    "Progress": {
      "properties": {
        "eta_seconds": {
          "format": "int32",
          "type": "integer"
        },
        "percent": {
          "format": "int32",
          "type": "integer"
        },
        "summary": {
          "type": "string"
        }
      },
      "type": "object"
    },
    "PublishReviewAnswer": {
      "properties": {
        "approve": {
          "description": "REQUIRED. Send `true` or `false` explicitly. A body that OMITS this key is refused with 400 `approve_required`; it is never read as `false`. A silent `false` here withholds a FINISHED FEED and ends the run — the single most expensive way for a client bug to be invisible. If your generated client models this as a plain boolean defaulting to false, fix the client. `null` earns the same 400, so the member is typed as a plain boolean and not a nullable one.",
          "type": "boolean"
        },
        "cadence": {
          "type": "string"
        },
        "delivery_target": {
          "items": {
            "type": "string"
          },
          "type": "array"
        },
        "notes": {
          "type": "string"
        }
      },
      "required": [
        "approve"
      ],
      "type": "object"
    },
    "PublishReviewOptions": {
      "properties": {
        "feed_name": {
          "type": "string"
        },
        "fields": {
          "items": {
            "$ref": "#/$defs/Field"
          },
          "type": "array"
        },
        "proposed_schedule": {
          "type": "string"
        },
        "rows_written": {
          "format": "int32",
          "minimum": 0,
          "type": "integer"
        },
        "sample_rows": {
          "description": "Rows as ARRAYS, positionally aligned with `fields`.",
          "items": {
            "items": {},
            "type": "array"
          },
          "type": "array"
        },
        "source_url": {
          "type": "string"
        }
      },
      "required": [
        "feed_name",
        "rows_written"
      ],
      "type": "object"
    },
    "Question": {
      "properties": {
        "choices": {
          "description": "Empty means the question is open — a typed answer.",
          "items": {
            "$ref": "#/$defs/Choice"
          },
          "type": "array"
        },
        "id": {
          "type": "string"
        },
        "prompt": {
          "type": "string"
        }
      },
      "required": [
        "id",
        "prompt"
      ],
      "type": "object"
    },
    "QuestionnaireAnswer": {
      "properties": {
        "answers": {
          "additionalProperties": {
            "type": "string"
          },
          "description": "Keyed by `Question.id`. A question you are SKIPPING must be ABSENT, never present with an empty value — the brain has to be able to tell a skip from an answer. At least one entry is required on a request.",
          "type": "object"
        }
      },
      "required": [
        "answers"
      ],
      "type": "object"
    },
    "QuestionnaireOptions": {
      "properties": {
        "questions": {
          "items": {
            "$ref": "#/$defs/Question"
          },
          "type": "array"
        }
      },
      "required": [
        "questions"
      ],
      "type": "object"
    },
    "Report": {
      "properties": {
        "feed_name": {
          "type": "string"
        },
        "fields": {
          "items": {
            "$ref": "#/$defs/Field"
          },
          "type": "array"
        },
        "first_run_ok": {
          "type": "boolean"
        },
        "merged": {
          "type": "boolean"
        },
        "notes": {
          "type": "string"
        },
        "rows_written": {
          "format": "int32",
          "minimum": 0,
          "type": "integer"
        },
        "sample_rows": {
          "description": "Rows as ARRAYS, positionally aligned with `fields` — never objects keyed by column name, which would silently drop a duplicate or reordered column instead of preserving the positions the producer promised.",
          "items": {
            "items": {},
            "type": "array"
          },
          "type": "array"
        },
        "schedule": {
          "type": "string"
        },
        "source_url": {
          "type": "string"
        }
      },
      "required": [
        "feed_name",
        "rows_written"
      ],
      "type": "object"
    },
    "SchemaColumnEdit": {
      "properties": {
        "drop": {
          "description": "Omitted means KEEP the column — the non-destructive default. That is the opposite of `approve`, whose omitted default would be the destructive one, which is why `approve` is refused when absent and this is not.",
          "type": "boolean"
        },
        "key": {
          "type": "string"
        },
        "rename_to": {
          "type": "string"
        },
        "required": {
          "type": "boolean"
        }
      },
      "required": [
        "key"
      ],
      "type": "object"
    },
    "SchemaReviewAnswer": {
      "properties": {
        "approve": {
          "description": "REQUIRED. Send `true` or `false` explicitly. A body that OMITS this key is refused with 400 `approve_required`; it is never read as `false`. The field is presence-tracked on purpose: a silent `false` at a review gate is not \"unanswered\" but a considered refusal that withholds the work and ends the run, with nothing recording that nobody decided. If your generated client models this as a plain boolean defaulting to false, fix the client — an absent decision must never reach this API as a decline. `null` is not an escape either: it earns the same 400, which is why this schema types the member as a plain boolean rather than a nullable one.",
          "type": "boolean"
        },
        "edits": {
          "items": {
            "$ref": "#/$defs/SchemaColumnEdit"
          },
          "type": "array"
        },
        "notes": {
          "type": "string"
        }
      },
      "required": [
        "approve"
      ],
      "type": "object"
    },
    "SchemaReviewOptions": {
      "properties": {
        "decision_summary": {
          "type": "string"
        },
        "proposed_fields": {
          "items": {
            "$ref": "#/$defs/Field"
          },
          "type": "array"
        },
        "sample_rows": {
          "description": "Rows as ARRAYS, positionally aligned with `proposed_fields`.",
          "items": {
            "items": {},
            "type": "array"
          },
          "type": "array"
        }
      },
      "required": [
        "proposed_fields"
      ],
      "type": "object"
    },
    "SourceCandidate": {
      "properties": {
        "rationale": {
          "type": "string"
        },
        "recommended": {
          "type": "boolean"
        },
        "score": {
          "format": "double",
          "type": "number"
        },
        "title": {
          "type": "string"
        },
        "url": {
          "type": "string"
        }
      },
      "required": [
        "url",
        "recommended"
      ],
      "type": "object"
    },
    "SourcePickerAnswer": {
      "properties": {
        "chosen_url": {
          "description": "Set exactly one of `chosen_url` or `re_discover`. Both, or neither, is a 400 (`source_answer_ambiguous`).",
          "type": "string"
        },
        "re_discover": {
          "type": "boolean"
        }
      },
      "type": "object"
    },
    "SourcePickerOptions": {
      "properties": {
        "candidates": {
          "items": {
            "$ref": "#/$defs/SourceCandidate"
          },
          "type": "array"
        }
      },
      "required": [
        "candidates"
      ],
      "type": "object"
    }
  }
}