Skip to content

ADR 0008 — Stateless variables provider; runner-owned state & type metadata

Status: Accepted (retrospective — behaviour since 2026-06)
Date: 2026-06-22
Supersedes (in part): provider seeding / bucket storage described in ADR 0002 § Current baseline
Related: ADR 0002, ADR 0004, ADR 0007, Context variables


Context

The variables capability provider originally exposed stateful endpoints (variables.set, variables.get, variables.list) with optional Redis backing and run-scoped buckets (X-Root-Run-ID). The orchestrator seeded scoped_context.init values into that store at run start.

That split caused problems:

  • Duplicate source of truth — values lived in orchestrator context and the provider.
  • Resume / checkpoint ambiguity — which store is authoritative after a crash?
  • Operational overhead — Redis buckets, dashboard variables-list proxy, provider isolation semantics.

Product direction: the workflow runner owns variable values and type declarations; the variables service is a stateless operation provider (concat, add, future format/transform helpers).


Decision

1. Runner-owned state

Concern Owner Mechanism
Runtime values Orchestrator run context scoped_context.init merged via apply_scoped_context_to_run_context; updates via context_set, json_set, step output_key, etc.
Type declarations Orchestrator run context Reserved key _scoped.declarations — JSON array of {name, type} from active scoped_context (workflow or inner scope / parallel lane).
Provider metadata Orchestrator run context _scoped.provider_service, _scoped.provider_version, …

init in workflow YAML remains required for typed slots: it supplies the initial value and declares the type used for validation and designer UX.

2. Stateless variables provider

Removed capabilities / endpoints:

  • variables.set, variables.get, variables.list
  • Redis / in-memory variable buckets

Retained capabilities:

  • variables.scoped_context — registry metadata for designer (metadata.designer.scoped_declarations)
  • variables.string_concat, variables.numbers_add — stateless operations; operands arrive in the POST body

The provider does not store per-run variable values.

3. Type metadata on variables.* service calls

Types are not inferred from payload shape alone when a step binds a declared variable by name.

Layer A — orchestrator (implemented):

  1. Before a service_call to variables.*, resolve whether payload_from references context:<name> / var:<name>.
  2. If <name> appears in _scoped.declarations, compare its declared type to the operation’s expected operand kind:
  3. variables.string_concatstring
  4. variables.numbers_addnumber
  5. Mismatch → fail the step with a clear runtime error (no HTTP call).

Layer B — HTTP header (implemented):

On every service_call where capability_name starts with variables., the orchestrator sends:

X-Scoped-Declarations: [{"name":"tenant_label","type":"string"}, …]

Value = current _scoped.declarations from the active context view (respects parallel lane shadowing). Lets the provider enforce richer rules on future operations that accept variable name references in the body.

Layer C — provider payload schema (implemented):

FastAPI/Pydantic models enforce structural types (parts: list[str], numbers: list[float]) independent of scoped_context.

Layer D — design-time validation (partial):

POST /validate/workflow-step on the variables service checks static JSON payloads for known operations. Full cross-step binding checks (e.g. payload_from: context:run_counter on string_concat) remain orchestrator responsibility (Layer A).

4. Logging & observability

Structured log workflow_run_scoped_context at run start:

Field Meaning
workflow Workflow name
scoped_declaration_count Number of declared variables
provider_service scoped_context.provider.service_name
scoped_context_applied true when declarations were merged into context

Removed: scoped_context_seeded (no provider HTTP seed).

5. Checkpoint / resume

Checkpoint stores orchestrator context (including init values and _scoped.declarations). No provider resync. See ADR 0007 §5.

6. Dashboard

GET /api/runtime/variables-list/{root_run_id} (proxy to provider list) is obsolete — run-scoped values are visible via run context / trace, not a provider bucket. Removal from dashboard is a follow-up task.


Consequences

  • Positive: Single source of truth; simpler provider; resume semantics align with context checkpoint.
  • Positive: Type discipline for variables.* + context:<declared-var> bindings without provider state.
  • Trade-off: Operations must receive values in the request body (or orchestrator resolves payload_from before POST). There is no variables.get.
  • Work (future): Extend Layer A/D for multi-field payloads; capability metadata declaring operand_kind per operation; designer warnings on mismatched bindings.

Example

scoped_context:
  provider: { service_name: variables.service }
  variables:
    - name: run_counter
      type: number
      init: { value: 12 }

steps:
  - id: add_one
    type: service_call
    capability_name: variables.numbers_add
    path: /variables/numbers-add
    payload: '{"numbers":[1,2]}'          # OK — literal numbers in body

  - id: bad_concat
    type: service_call
    capability_name: variables.string_concat
    path: /variables/string-concat
    payload_from: context:run_counter     # FAIL at runtime — number decl + string op

See also