{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/aimeat-io/json-schema/aimeat-io-workflow-def-input-schema.json",
  "title": "WorkflowDefInput",
  "x-generated": "2026-11-01",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/aimeat-io-openapi.json#/components/schemas/WorkflowDefInput",
  "type": "object",
  "required": [
    "title",
    "description",
    "trigger",
    "vars",
    "steps",
    "on_step_fail"
  ],
  "properties": {
    "title": {
      "$ref": "#/$defs/LocalizedString"
    },
    "description": {
      "$ref": "#/$defs/LocalizedString"
    },
    "trigger": {
      "$ref": "#/$defs/WorkflowTrigger"
    },
    "vars": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/WorkflowVar"
      },
      "description": "Declared run parameters. Two vars are ALWAYS available to key templates without declaration: {run} (the run id) and {date} (the run date, YYYY-MM-DD). Template deliverable/signal keys with one of these to give each run its own keyspace (run-scoped keys) — the non-destructive way to keep re-runs from seeing a prior run's output. A declared var of the same name wins. A run an event trigger starts also has the event as variables, in key templates and sent to an agent step as `var.<name>` scope entries: `event_on`, `event_at` (ISO 8601), `event_folded` (how many events the run stands for), and by event `event_key` (memory.write), `event_offer` (offer.ordered), or `event_organism`, `event_workspace`, `event_space`, `event_count` and `event_row_ids` (workspace.rows: one run per append call; at most 50 ids, comma-separated, and only ids made of letters, digits and `. _ : @ + = -`). An ecosystem.event run has `event_app` and the payload fields its own `match` names."
    },
    "steps": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/WorkflowStep"
      }
    },
    "on_step_fail": {
      "type": "string",
      "enum": [
        "inspect"
      ]
    },
    "notify_on_finish": {
      "type": "boolean",
      "description": "Owner opt-in. When true, a full-live run reaching a terminal state (done / partial / cancelled) drops a finish notification — the in-app inbox always, plus an email when the owner has a notification email and SMTP is configured — with the outcome and a per-step log. Fires for both success and failure. Default false."
    },
    "resume": {
      "type": "boolean",
      "description": "Owner opt-in (default false). Re-evaluate the DAG against reality instead of restart-and-skip: a downstream step gates on its OWN required_to_function (checked against current memory) rather than requiring every `after` dependency to have succeeded, so `after` becomes ordering and the input gate becomes the real gate. A dependent whose input is present runs even if a parent step timed out / went red; one whose input is missing goes input-red (never blanket-skipped). Safe only when crew stages are idempotent. Independent of this flag, the watchdog always re-checks a step's success_signal before failing it (a slow step whose output arrived late recovers to green) and only times out after timeout_min of NO progress."
    },
    "fresh": {
      "type": "boolean",
      "description": "Owner opt-in (default false). Before a step is first dispatched, delete the memory keys its success_signal checks (minus any it also reads as input), so an idempotent skip-existing crew regenerates them from empty instead of finding the previous run's output already present (which would false-green the step and waste a no-op crew pass). Use when a workflow deliberately writes to the SAME keys every run and wants each run to overwrite. DESTRUCTIVE — discards the prior run's deliverables. Cleared once up front (not per-step) so parallel steps sharing an output namespace can't wipe each other's fresh output; pure external inputs are kept. The non-destructive alternative is per-run keys via the built-in vars {run} (the run id) and {date} (the run date), always available to key templates without declaration, which keep history. Orthogonal to resume."
    },
    "parallel": {
      "type": "boolean",
      "description": "Owner opt-in (default false). Two or more live runs of this workflow may be in flight at once. By default a second start while one is running does nothing and answers `skipped: true` with the running run's id, because the steps write to templated keys and two runs of a definition whose keys carry nothing run-specific would write over each other. Set it when the keys DO distinguish runs (a case reference in vars, the built-in {run}). Refused together with fresh, which deletes the produced keys at run start."
    },
    "skip_done": {
      "type": "boolean",
      "description": "Owner opt-in (default false). Before dispatching a ready step, check its success_signal against current memory; if the deliverable is already present, mark the step green WITHOUT dispatching the crew. So a re-run continues from the not-yet-done steps instead of re-invoking crews for completed work — and re-running a single step = delete its output key(s) then run (every other step skips, only the cleared one + its dependents re-run). Safe only when a present deliverable genuinely means \"done\" (idempotent producers); a workflow whose crew is meant to OVERWRITE stable keys each run should use fresh instead. Moot with fresh (which clears outputs at run start). Orthogonal to resume."
    },
    "llm": {
      "type": "object",
      "description": "Owner consent to use the node OpenRouter for `llm` signal leaves.",
      "properties": {
        "approved": {
          "type": "boolean"
        }
      }
    },
    "maxCostUsd": {
      "type": [
        "number",
        "null"
      ],
      "exclusiveMinimum": 0,
      "maximum": 10000,
      "description": "The most one run may spend on the owner's AI, in US dollars: through its `ai` steps, and through the node's model judging its `llm` signals. The node adds up what each call cost, as it recorded them. Before an ai step's model call starts, the node sets aside what one attempt of the step is expected to cost (`estimateUsd` on the run step: the most one attempt cost in the workflow's last ten finished runs that ran it with the same action; with no such run, an equal share of the cap nobody holds, split across the ai steps not yet started). The call starts only when what the run has spent, what its open calls hold and that estimate stay within this, so ai steps that fit together still run side by side. A call holds its share until it answers (`openCalls` on the run step), also when its step was moved on meanwhile by a timeout, a retry or a cancel; a retry's call beside one still open holds its own share. A step that does not fit stays `pending` while a call is open; with none open, the run ends with status `stopped`, `costCap` holds the cap, the spend and the step's estimate, and `reason` says it in words. A step expected to cost more than this whole amount starts alone, when no call is open and the run has spent less than this, and what it really cost counts as usual. A call already started finishes and keeps its result, and one that costs more than its estimate takes the run past this by the difference. Once the spend has reached this the judge is not asked, and its `llm` leaf passes as it does when the judge is unavailable. Absent or null: no cap. The owner's daily AI budget bounds every AI call as well."
    },
    "costCapMorsels": {
      "type": [
        "integer",
        "null"
      ],
      "deprecated": true,
      "description": "Deprecated 2026-09-25 and removed in 4.0.0; nothing ever read it: a morsel paces what agents store and is not money, so it cannot cap a spend. Until 4.0.0 a save that sets it still succeeds, does not keep it, and answers with `warnings` naming maxCostUsd and with the Deprecation headers. From node 4.0.0 a save that sets it is refused (400 WORKFLOW_INVALID)."
    }
  },
  "$defs": {
    "LocalizedString": {
      "description": "A plain string, or a locale-keyed map (e.g. en_US / fi_FI text).",
      "oneOf": [
        {
          "type": "string"
        },
        {
          "type": "object",
          "additionalProperties": {
            "type": "string"
          }
        }
      ]
    },
    "WorkflowSignal": {
      "type": "object",
      "description": "A signal tree evaluated against owner memory, with {var} templated from the run params. A leaf is deterministic ({ kind: deterministic, key|key_glob, op: exists|nonempty| count_nonempty|json_valid|json_schema|json_field, … }) or llm ({ kind: llm, key|key_glob, ask }); composites are { all: [...] }, { any: [...] }, or { when, then }. See workflow-schemas.ts for the full grammar.\n",
      "additionalProperties": true
    },
    "WorkflowStep": {
      "type": "object",
      "required": [
        "id",
        "description"
      ],
      "properties": {
        "id": {
          "type": "string"
        },
        "agent": {
          "description": "One agent name, or a list (a list runs in parallel within the step). Required (with `offer`) for the default agent step; absent for export-out / trigger-geai / human-input action steps.",
          "oneOf": [
            {
              "type": "string"
            },
            {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          ]
        },
        "offer": {
          "type": "string",
          "description": "Inherit success_signal + required_to_function from this offer (agent steps)."
        },
        "after": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Dependency order (DAG)."
        },
        "description": {
          "$ref": "#/$defs/LocalizedString"
        },
        "required_to_function": {
          "description": "Input gate (consumer-owned). A signal, or the literal 'none'. Inherited from the offer if omitted.",
          "oneOf": [
            {
              "$ref": "#/$defs/WorkflowSignal"
            },
            {
              "type": "string",
              "enum": [
                "none"
              ]
            }
          ]
        },
        "success_signal": {
          "$ref": "#/$defs/WorkflowSignal"
        },
        "retry": {
          "type": "object",
          "description": "Not allowed on human-input steps (the timeout policy owns the fallback there).",
          "properties": {
            "max": {
              "type": "integer"
            },
            "backoff_min": {
              "type": "number"
            }
          }
        },
        "timeout_min": {
          "type": "integer",
          "description": "Max minutes to wait for this step's success signal before timed-out. Default 60 (agent/ecosystem steps) / 1440 (human-input steps)."
        },
        "action": {
          "type": "object",
          "description": "Absent ⇒ the default agent-dispatch. `export-out` pushes a memory key to a GEAI; `trigger-geai` invokes a GEAI capability (both complete on the tunnel reply). `human-input` parks the run (step state `waiting-human`) and asks the owner a structured question (in-app inbox + push); the run resumes when the owner answers via POST /v1/workflows/{id}/runs/{runId}/steps/{stepId}/answer, or the `on_timeout` policy fires after timeout_min (default 1440 = 24h): fail (timed-out, default) | skip | default (synthesize the answer `default_option`, by \"timeout-default\"). The answer JSON ({picks, pick, other, answeredAt, by}) is written to `answer_to_key` (templated, sandbox-prefix-honoring) so downstream steps branch on it with deterministic json_field gates — e.g. required_to_function {op: json_field, key: <answer_to_key>, path: pick, equals: approve}. `extension` runs one of the OWNER'S OWN extension actions on this node, in the sandbox, with no agent session and no model call — the deterministic half of a pipeline (fetch, normalise, hash, write a file) next to the agent steps that interpret. It completes through the same path as an ecosystem step, so its `success_signal` decides green or red; a `success_signal` is REQUIRED on an extension step, because without one the step would go green whenever the script merely returned, whatever it produced. Naming an extension installed by anyone else is refused at save AND at run: an unattended call has no paywall, no contract and no meter, so it would be an unlimited standing call on someone else's capability, API keys and quota.",
          "required": [
            "kind"
          ],
          "properties": {
            "kind": {
              "type": "string",
              "enum": [
                "agent",
                "export-out",
                "trigger-geai",
                "human-input",
                "extension",
                "datapackage",
                "ai"
              ]
            },
            "prompt": {
              "type": "string",
              "maxLength": 20000,
              "description": "ai: the prompt itself, templated with the run's vars. Use prompt_key instead when the wording should be changeable without editing the workflow."
            },
            "prompt_key": {
              "type": "string",
              "maxLength": 400,
              "description": "ai: owner-namespace key holding the prompt text (a string, or an object with a `prompt` string). Templated with the run's vars, and so is the text it holds. This is what makes a text stage cheap to change: the wording is a memory record, so editing it touches no code, no agent fleet and no deploy."
            },
            "input_keys": {
              "type": "array",
              "maxItems": 20,
              "items": {
                "type": "string",
                "maxLength": 400
              },
              "description": "ai: owner-namespace keys read at run time and appended to the prompt, labelled by key and templated with the run's vars. The model behind an ai step has no tools, so the prompt string is the whole of what it can see: a prompt that ASKS it to read a record is a prompt that invites it to invent one. Name the step's inputs here. A key with no record is stated as missing in the prompt rather than left silent."
            },
            "json": {
              "type": "boolean",
              "description": "ai: parse the answer as JSON before writing it, so a malformed answer fails the step instead of becoming a string nobody validates."
            },
            "model": {
              "type": "string",
              "maxLength": 200,
              "description": "ai: override the owner's default model for this one step."
            },
            "op": {
              "type": "string",
              "enum": [
                "text",
                "image",
                "transcribe"
              ],
              "description": "ai: what the step does (default text). image writes { storage_key, url, mime_type, model } to result_to_key; transcribe turns the audio at audio_key into text (with json, { text, language, seconds, model }) and needs no prompt."
            },
            "provider": {
              "type": "string",
              "maxLength": 200,
              "description": "ai: one of the owner's provider ids, or a type. No fallback then."
            },
            "role": {
              "type": "string",
              "minLength": 1,
              "maxLength": 300,
              "description": "ai: the AI role the step's call runs as, one of the owner's role ids (GET /v1/ai/roles). A named model or provider wins over it."
            },
            "audio_key": {
              "type": "string",
              "maxLength": 400,
              "description": "ai, op transcribe: the audio file's key in the owner's storage, templated with the run's vars."
            },
            "language": {
              "type": "string",
              "maxLength": 12,
              "description": "ai, op transcribe: ISO-639-1 hint."
            },
            "size": {
              "type": "string",
              "maxLength": 20,
              "description": "ai, op image: the picture size."
            },
            "geai": {
              "type": "string",
              "description": "export-out / trigger-geai: the target GEAI."
            },
            "capability": {
              "type": "string",
              "description": "trigger-geai (required) / export-out (default __deposit__)."
            },
            "from": {
              "type": "string",
              "description": "export-out: owner memory key whose value is pushed."
            },
            "input": {
              "type": "object",
              "additionalProperties": true,
              "description": "trigger-geai: capability input. extension: the action's input, with `{var}` substituted into its string leaves at dispatch (numbers and booleans pass through unchanged)."
            },
            "input_from": {
              "type": "object",
              "additionalProperties": {
                "type": "string",
                "maxLength": 400
              },
              "description": "extension: `{ param: key }`, at most 20. At dispatch each param of the action's input is set to the value of that owner-namespace key (templated, sandbox-prefix-honoring), over `input`; a missing key arrives as null. This is how an extension step acts on an earlier step's `result_to_key` or a person's `answer_to_key`, which the sandbox cannot read."
            },
            "extension": {
              "type": "string",
              "maxLength": 120,
              "description": "extension: the extension name — must be installed by this workflow's owner."
            },
            "action": {
              "type": "string",
              "maxLength": 120,
              "description": "extension: the action id on that extension."
            },
            "instance_id": {
              "type": "string",
              "maxLength": 120,
              "description": "extension: run against this named instance (its config, secrets decrypted, arrives as ctx.instance)."
            },
            "result_to_key": {
              "type": "string",
              "maxLength": 400,
              "description": "extension / ai: owner-namespace key (templated, sandbox-prefix-honoring) the action's RETURN VALUE — for an ai step, the model's answer — is written to, so a signal can gate on it — the analogue of `answer_to_key`. It is needed because an extension's own memory lives in the `ext:{name}` namespace while a workflow's signals read owner scope, and those never intersect: a signal pointed at what the extension wrote for itself can only read nothing. Absent `success_signal`, the default gate is that this key is non-empty. A step must declare one or the other."
            },
            "name": {
              "type": "string",
              "maxLength": 64,
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "description": "datapackage: the package name. It becomes part of the permanent address, so it is a name rather than a sentence."
            },
            "from_key": {
              "type": "string",
              "maxLength": 400,
              "description": "datapackage: the owner-namespace key an earlier step wrote (its `result_to_key`), {var}-templated. This is the join a repeating package needs and the one no other component can make: an extension step lands its result in the owner's namespace as a PRIVATE record, which the sandbox cannot read back (ctx.memory.get sees `ext:{name}`, getPublic returns only public records). The engine already runs as the owner and already wrote that key."
            },
            "rows_at": {
              "type": "string",
              "maxLength": 200,
              "description": "datapackage: dotted path to the rows INSIDE that value. A producer answers with an envelope (`{ ok, total, results }`) far more often than with a bare array, so naming the path is how a workflow says which part of the answer is the table. Omit when the value is the array itself."
            },
            "paging": {
              "type": "object",
              "description": "extension: call the action repeatedly and merge the pages. Real registries page — `laake-fi` caps at 500 rows on a set of 718, so one call answers `truncated: true` and a package built from it holds five-sevenths of the data with nothing saying so. The merged value keeps the LAST page's envelope with `items_at` replaced by every row collected, plus `pagesFetched` and `complete`. `complete` is false when the loop stopped at `max_pages` rather than at the end of the data, which is what makes \"did we get all of it\" something a success_signal can assert.",
              "required": [
                "offset_param",
                "page_size",
                "items_at",
                "max_pages"
              ],
              "properties": {
                "offset_param": {
                  "type": "string",
                  "description": "Input field carrying the offset; set on each call."
                },
                "page_size": {
                  "type": "integer",
                  "description": "Rows per page, and the increment."
                },
                "items_at": {
                  "type": "string",
                  "description": "Dotted path to a page's rows."
                },
                "total_at": {
                  "type": "string",
                  "description": "Dotted path to the reported total. Absent = stop on a short page."
                },
                "max_pages": {
                  "type": "integer",
                  "description": "Hard stop. Required: a producer that never reports completion must not loop forever."
                }
              }
            },
            "for_each": {
              "type": "object",
              "description": "extension: call the action once per value and merge the answers — the other shape a real producer has. `kumppani` answers about ONE company per call, so a package covering ten companies is ten calls. Paging varies an offset over one query; this varies a parameter over a list. Both merge `items_at` and both add `complete`. A call that fails fails the STEP: ten companies of which one could not be read is not a package about ten companies.",
              "required": [
                "values",
                "param",
                "items_at"
              ],
              "properties": {
                "values": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "description": "The values to iterate; each is {var}-templated."
                },
                "param": {
                  "type": "string",
                  "description": "Input field each value is set on."
                },
                "items_at": {
                  "type": "string",
                  "description": "Dotted path to the rows in ONE answer."
                }
              }
            },
            "union": {
              "type": "array",
              "description": "datapackage: several lists in one answer, published as ONE table with a discriminator. `aiuutiset` answers with topics, actors and sources — the same numbers under a differently-named label each — and `rows_at` names exactly one path, so without this such a producer could only publish a third of what it knows. Each source gets its own `columns` (that is what reconciles topic/actor/source into one `name`) and its own `set` of constant columns (that is the discriminator). When `union` is present the top-level `rows_at` and `columns` are not used.",
              "items": {
                "type": "object",
                "required": [
                  "rows_at"
                ],
                "properties": {
                  "rows_at": {
                    "type": "string"
                  },
                  "columns": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    }
                  },
                  "set": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "Constant columns added to every row from this source."
                  }
                }
              }
            },
            "columns": {
              "type": "object",
              "additionalProperties": {
                "type": "string"
              },
              "description": "datapackage: column name to a dotted path INSIDE each row. Omit to publish the rows as they are. A Table Schema describes scalars and a real producer answers with nested objects (`buyer: { name, businessId }`) and arrays, so this is the transformation these bindings actually need — the first production run of one refused 200 row problems for exactly that reason. It is a mapping rather than a script on purpose: flattening is declarative and lands in the descriptor as a recorded transformation, where a scripting language in a workflow descriptor would be a sandbox with no boundary. A missing path yields null rather than dropping the column, so a row that lost a field is a visible gap instead of a silently different table; an array at the end of a path is joined with a semicolon."
            },
            "changes": {
              "type": "string",
              "maxLength": 2000,
              "description": "datapackage: what moved against the previous version and why, {var}-templated. Required, and required at SAVE as well as at publish — a workflow that discovers the contract at 06:00 has already lost the run it was written for."
            },
            "schema": {
              "type": "object",
              "description": "datapackage: the Table Schema the rows must satisfy. Omitting it INFERS from the rows, and for a repeating producer that is the wrong default even though it is the convenient one: inference widens to fit whatever arrived, so a run where the upstream sent a word instead of a number publishes a version whose column is quietly a string, and every consumer's join against it stops matching with no error anywhere. Declared, the same run is refused with the row and the field named, and the package stands on its previous version.",
              "properties": {
                "fields": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "required": [
                      "name",
                      "type"
                    ],
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            },
            "resource": {
              "type": "string",
              "maxLength": 64,
              "description": "datapackage: resource name, default `rows`."
            },
            "provenance": {
              "type": "object",
              "additionalProperties": true,
              "description": "datapackage: sources, licence, legal basis and transformations, carried into the descriptor."
            },
            "retention_policy": {
              "type": "object",
              "description": "datapackage: how many old versions to keep.",
              "properties": {
                "keep": {
                  "type": "integer",
                  "minimum": 0
                },
                "unit": {
                  "type": "string"
                }
              }
            },
            "question": {
              "type": "object",
              "description": "human-input: the structured question (AskUserQuestion-shaped).",
              "required": [
                "prompt",
                "options"
              ],
              "properties": {
                "header": {
                  "type": "string",
                  "maxLength": 80
                },
                "prompt": {
                  "type": "string",
                  "maxLength": 2000,
                  "description": "{var}-templated at ask time."
                },
                "options": {
                  "type": "array",
                  "minItems": 1,
                  "maxItems": 20,
                  "items": {
                    "type": "object",
                    "required": [
                      "id",
                      "label"
                    ],
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "label": {
                        "type": "string"
                      }
                    }
                  }
                },
                "multiSelect": {
                  "type": "boolean"
                },
                "allowOther": {
                  "type": "boolean"
                }
              }
            },
            "answer_to_key": {
              "type": "string",
              "description": "human-input: memory key template the answer JSON is written to."
            },
            "reviews_key": {
              "type": "string",
              "description": "human-input: the memory key whose CONTENT this question puts in front of the person. It is the only field in the workflow engine that can upgrade a provenance record's humanInvolvement to `editorial-control`, because it is the only place where a named person reads the substance and can reject it. On an answer the reviewed content is re-stamped with that person recorded as the reviewer. A step that merely asks \"publish now?\" must NOT set it — clicking publish is not review, and a false editorial-control claim is worse than none. A watchdog timeout default (by \"timeout-default\") never upgrades anything: nobody read it on that path."
            },
            "on_timeout": {
              "type": "string",
              "enum": [
                "fail",
                "skip",
                "default"
              ],
              "description": "human-input timeout policy (default fail)."
            },
            "default_option": {
              "type": "string",
              "description": "human-input: required when on_timeout=default; must be one of question.options[].id."
            }
          }
        }
      }
    },
    "WorkflowTrigger": {
      "type": "object",
      "required": [
        "kind"
      ],
      "properties": {
        "kind": {
          "type": "string",
          "enum": [
            "schedule",
            "manual",
            "event",
            "ecosystem.event"
          ]
        },
        "cron": {
          "type": "string",
          "description": "schedule kind"
        },
        "timezone": {
          "type": "string"
        },
        "on": {
          "type": "string",
          "description": "event kind: one of memory.write, offer.ordered, workspace.rows. ecosystem.event kind: the bound app's own event name."
        },
        "match": {
          "type": "object",
          "additionalProperties": {
            "type": "string"
          },
          "description": "Globs the event is tested against. memory.write: `key`. offer.ordered: `offer`. workspace.rows: `organism` and `workspace` (both required, refused at save without them) and optionally `space`, tested against the row space's name and its namespace. ecosystem.event: fields of the app's payload."
        },
        "coalesce": {
          "type": "boolean",
          "description": "event and ecosystem.event kinds. One run of a workflow is live at a time, and an event that arrives during a run starts nothing. With `coalesce: true` every event that arrives during a run is kept, and ONE more run starts when the live run ends: its `event_folded` variable says how many events it stands for, `event_at` is the time of the first, a row count is summed and row ids are joined. Refused beside `parallel`. Do not set it on a workflow whose own step writes what its trigger matches."
        },
        "app": {
          "type": "string",
          "description": "ecosystem.event kind: the bound app name"
        },
        "version": {
          "type": "integer",
          "description": "ecosystem.event kind: the pinned major version of the event"
        }
      }
    },
    "WorkflowVar": {
      "type": "object",
      "required": [
        "name",
        "type",
        "description"
      ],
      "properties": {
        "name": {
          "type": "string"
        },
        "type": {
          "type": "string",
          "description": "'date' | 'enum[a,b]' | 'string' | …"
        },
        "description": {
          "$ref": "#/$defs/LocalizedString"
        },
        "default": {
          "type": "string"
        }
      }
    }
  }
}