Skip to content

Input/Output Schema Validation

A schema is a contract on the shape of data crossing a boundary. Without one, a missing field is discovered by whatever task first dereferences it — usually several tasks in, as a NullPointerException in a worker or a silently-null ${...} expression. With one, the execution is rejected at the boundary, before any side effect.

Where a schema attaches

Attachment point Scope Fields
Workflow definition The workflow's own input and output WorkflowDef.inputSchema / outputSchema
Task definition Every use of that task, in every workflow TaskDef.inputSchema / outputSchema

Put a schema on the task definition when the contract belongs to the task — every workflow calling charge_payment should agree on what a payment request looks like. Put it on the workflow definition when the contract belongs to the entry point, which is the case for anything triggered by an external caller.

Schema shape

The schema is a SchemaDef, embedded in the definition:

{
  "name": "customerInput",
  "version": 1,
  "type": "JSON",
  "data": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
      "customerId": { "type": "string" },
      "tier": { "type": "string", "enum": ["standard", "premium"] }
    },
    "required": ["customerId"],
    "additionalProperties": false
  }
}
Field Meaning
name Identifier for the schema
version Defaults to 1. Lets a schema evolve alongside the definition that uses it
type JSON, AVRO, or PROTOBUF
data The schema document itself
externalRef Reference to a schema held outside Conductor, instead of inline data

Attaching it to a workflow

{
  "name": "order_fulfillment",
  "version": 1,
  "ownerEmail": "team@example.com",
  "schemaVersion": 2,
  "enforceSchema": true,
  "inputSchema": {
    "name": "customerInput",
    "version": 1,
    "type": "JSON",
    "data": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "type": "object",
      "properties": { "customerId": { "type": "string" } },
      "required": ["customerId"],
      "additionalProperties": false
    }
  },
  "tasks": []
}

Two fields are easy to confuse. schemaVersion is unrelated to any of this — it is the workflow definition format version and should be 2. enforceSchema is the switch that turns validation on; on WorkflowDef it defaults to true, so a definition that declares an inputSchema is validated unless you explicitly set it to false. On TaskDef it defaults to false.

Register it the usual way — schemas travel inside the definition, so there is no separate step:

curl -X PUT "$CONDUCTOR_SERVER_URL/metadata/workflow" \
  -H 'Content-Type: application/json' \
  -d '[ ... definition above ... ]'

When validation runs

Point Effect on failure
Workflow input The workflow does not start
Task input The task fails before the worker sees it
Task output The task fails after the worker returns
Workflow output The workflow fails at completion

Input validation is the valuable one: it rejects the execution before any task has run, so there is nothing to compensate for. Output validation catches a worker returning the wrong shape, which otherwise surfaces as a downstream failure far from its cause.

Writing schemas that age well

additionalProperties: false deserves a moment's thought. It turns an unexpected field into a hard failure — right for a contract you own end to end, a nuisance for one where callers legitimately pass extra context. Leave it out unless you mean it.

Adding a field to required is a breaking change for every existing caller. Because a running execution keeps the definition version it started with, the safe path is the same as any other definition change: register a new version rather than editing the current one. See Managing Workflow Versions.

Keep the schema narrow. A schema that restates every optional field becomes something nobody updates, and a stale contract is worse than none. Validate the fields whose absence would actually break the workflow.