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-listproxy, 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):
- Before a
service_calltovariables.*, resolve whetherpayload_fromreferencescontext:<name>/var:<name>. - If
<name>appears in_scoped.declarations, compare its declared type to the operation’s expected operand kind: variables.string_concat→ stringvariables.numbers_add→ number- 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_frombefore POST). There is novariables.get. - Work (future): Extend Layer A/D for multi-field payloads; capability metadata declaring
operand_kindper 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¶
- Plan — scoped context (variables) (updated baseline)
- Capability Provider Contract