ADR 0004 — Orchestrator: scoped_context per scope (lexical layers & shadowing)¶
Status: Proposed (concrete orchestrator direction — not implemented yet)
Date: 2026-05-02
Depends on: ADR 0003 — Workflow scope concept
Related: ADR 0002 — Scoped context plan (variables), Workflow YAML — Scoped context
Context¶
Today, scoped_context exists only on the workflow document. It declares variable slots and optional init values merged into the single workflow run context (current behaviour).
The intended model treats scoped_context as “context attached to a scope”: any compositional scope (starting with parallel lanes, later possibly other step containers) may carry the same declarative shape as workflow-level scoped_context, but scoped to that lexical unit.
Product requirement (example)
- At workflow (or parent scope) “workspace”, variable
testis declaredstring. - Inside a nested scope, authors must be able to declare
testagain as a different type (e.g.int)—a new binding in the inner scope, not a conflicting mutation of the outer declaration.
That implies:
- Lexical layers of context (stack or overlay), not one flat mutable map for nested scopes.
- Shadowing: inner declaration hides outer for name resolution while executing steps inside that scope.
- Exit behaviour: leaving the scope drops inner bindings so outer
testis visible again (unless explicitly exported via outputs — out of scope for this ADR).
Parallel scopes must not share one mutable context map if shadowing and type discipline are to hold; per-lane isolation becomes mandatory for type: parallel (aligns with ADR 0003).
Decision¶
1. Unified schema¶
Reuse the existing ScopedContext shape (provider + variables[] with name, type, optional init) for:
| Attachment point | Meaning |
|---|---|
WorkflowDoc.scoped_context |
Workflow-wide declarative slots (already implemented). |
ParallelScope.scoped_context (new) |
Declarative slots for that lane only (first delivery target). |
| Future: other scope-bearing nodes | Same schema where the product adds nested scopes. |
Naming: Keep the field name scoped_context everywhere so YAML, registry metadata, and designer stay aligned—only the parent object changes (workflow vs scopes[] entry).
2. Shadowing rules¶
- Within one
scoped_contextblock: unique variablename(existing rule). - Across nested scopes: same name allowed with different
type/init— inner wins for resolution while executing inside the inner scope. - Reserved prefixes (
_scoped.,__) remain global rules.
3. Resolution order (runtime)¶
When resolving a context key for a step inside a scope:
- Innermost active scope’s
scoped_contextinit / runtime updates (if any). - Enclosing scopes, outward.
- Workflow
scoped_contextand trigger/initial_context.
(Exact algorithm: overlay chain or immutable stack; implementation detail, but must be deterministic.)
4. Parallel (type: parallel)¶
- Each lane (
ParallelScope) that runs concurrently must execute with its own context view (fork/copy before concurrent entry, plus lane’sscoped_contextmerge). - No cross-lane reads/writes of lane-local shadowed keys.
5. Variables provider (stateless operations)¶
Variable values and types are owned by the orchestrator run context (ADR 0008). The variables provider exposes stateless variables.* operations only; there is no variables.set seed or per-run bucket.
- Parallel lane
scoped_contextmerges into the lane context view (already implemented). service_callsteps pass operand values in the POST body; orchestrator sendsX-Scoped-Declarationsfor type-aware operations.- No provider seed for any scope level.
Decision for phase 1: orchestrator context merge + shadowing + isolation for parallel lanes — done. Provider remains stateless; type checks use _scoped.declarations + operation kind.
Consequences¶
- Pydantic: extend
ParallelScopewith optionalscoped_context: ScopedContext | None. - Runner: refactor
_exec_parallel/_exec_substepsto pass a scope-aware context handle (not raw shared dict) for lanes; workflow body stays single-threaded sequential unless entering parallel. - Dashboard / designer: optional nested editor for
scopes[].scoped_contextafter schema ships. - Docs: clarify that “scoped_context” = declarative context on a scope; workflow-level is simply the root scope.
Implementation phases (suggested)¶
| Phase | Scope | Deliverable |
|---|---|---|
| A | Models + validation | ParallelScope.scoped_context in WorkflowDoc; validate; YAML docs + examples |
| B | Runtime (no provider) | Per-lane context fork; merge lane scoped_context; shadowing; exit pop |
| C | Type-aware operations | Orchestrator operand checks + X-Scoped-Declarations on variables.* calls (ADR 0008) |
| D | Designer | Nested table UX for parallel scopes |
Example (illustrative YAML)¶
name: demo
invocation: { type: http }
scoped_context:
provider: { service_name: variables.service }
variables:
- name: test
type: string
init: { value: "outer" }
steps:
- id: fork
type: parallel
scopes:
- name: lane_a
scoped_context:
provider: { service_name: variables.service }
variables:
- name: test
type: number
init: { value: 0 }
steps:
- id: in_lane
type: log
message: "lane sees int test, not outer string"
# outputs_map etc. unchanged
(Exact parallel wiring omitted for brevity.)
Out of scope (this ADR)¶
- Changing
workflow_callsemantics. - Non-parallel nested scopes until a follow-up ADR defines containers (e.g. explicit
sequencewithscoped_context). - Strong static typing at runtime: orchestrator enforces declared type vs
variables.*operation forpayload_from: context:<name>bindings; provider validates request body shape. See ADR 0008.
See also¶
- ADR 0003 — Workflow scope concept — boundaries and connections.
- ADR 0002 — Plan (variables) — current workflow-level delivery.