Skip to content

Workfile Format Overview

This section is informative.

Workfile is a declarative language for describing a workflow as data. A Workfile says which inputs a workflow accepts, which operations it composes, how values move between those operations, which control-flow decisions are available, and which values the workflow returns. It does not contain connector implementations, credentials, queues, worker configuration, or a storage design.

The format is deliberately readable as ordinary YAML, but it is more constrained than arbitrary YAML. Step names create immutable bindings, dotted keys identify connector actions, and wf. keys identify language constructs. Expressions appear inside {{ ... }} and can read values already in scope. These constraints let a validator understand the workflow, its dependencies, and many mistakes before an executor contacts an external system.

A useful mental model is that a Workfile is a typed composition plan. Connector manifests supply the types and operational promises of external actions. The Workfile supplies the workflow-specific choices: which action to call, which values to pass, what to do with the result, and what to return.

The following example looks up a support ticket, chooses an escalation path, inspects its related tickets, and returns a small result. The support.* operations are not defined by the Workfile; validation resolves them from a connector manifest.

workfile: 1
name: triage_ticket
owner: support_operations
version: 3
inputs:
ticket_id:
type: string
required: true
urgent:
type: bool
default: false
steps:
- ticket:
support.get_ticket:
id: "{{ inputs.ticket_id }}"
- triage:
wf.choose:
- when: "{{ inputs.urgent or ticket.priority == 'critical' }}"
then:
- escalation:
support.escalate:
id: "{{ ticket.id }}"
reason: Immediate review requested
idempotency_key: "{{ ticket.id }}"
else:
- note:
support.add_note:
id: "{{ ticket.id }}"
body: Ticket reviewed by the triage workflow.
- related:
wf.for_each: "{{ ticket.related_ids }}"
as: related_id
max: 20
concurrency: 4
on_iteration_fail: continue
retain: failures
steps:
- check:
support.get_ticket:
id: "{{ related_id }}"
- summary:
wf.value:
ticket_id: "{{ ticket.id }}"
status: "{{ ticket.status }}"
urgent: "{{ inputs.urgent }}"
related_checked: "{{ related.count }}"
related_failed: "{{ related.failed }}"
outputs:
result: "{{ summary }}"

Several details are worth noticing:

  • inputs describes the invocation boundary. The executor checks supplied values and applies defaults before the first step begins.
  • Each item in steps has one id, such as ticket or triage. A successful value-producing step makes that id readable by later steps.
  • support.get_ticket and the other dotted non-wf keys are connector actions. Their argument and result shapes come from the resolved support manifest.
  • The idempotency key is a domain value, so a second run for the same ticket does not escalate twice; run.id would protect only reissues within one run.
  • A scalar containing exactly one interpolation preserves the expression’s type. Thus "{{ inputs.urgent }}" is a boolean value, not the text "true" or "false".
  • wf.choose creates one selected branch scope. The branch-local escalation and note bindings are reachable through triage, subject to the branch-binding rules.
  • wf.for_each creates isolated iteration scopes. Its explicit max bounds work caused by the input, while concurrency permits up to four independent checks at once. count, succeeded, and failed remain available in every retention mode. retain: failures keeps failed scopes under failures; the default retain: all keeps every scope under items. Changing retention preserves summary access, but references to discarded scopes become invalid.
  • wf.value builds a new value without causing an external effect. outputs selects the public result only after successful completion.
  • A dotted key at the top of a step body always names an operation; a data map with a dotted key uses explicit wf.value.
  • An unresolved write result is ambiguous. The default on_unknown: abort terminates the run even under on_fail: continue, on_iteration_fail: continue, or on_branch_fail: continue. Choose on_unknown: fail to pass flow.action_ambiguous into catch, on_fail, and enclosing failure policy; this does not establish whether the external write occurred. See Core Ambiguity Disposition.
  • Dry runs suppress writes without delegated dry-run behavior and work that depends on suppressed results; independent work still executes.
  • Expressions cannot read an ambient clock. run.started_at is the fixed instant at which the run actually began; a workflow observes a later current instant through the canonical time.now action.

This example assumes the resolved manifest declares the referenced actions and fields, that ticket.related_ids is a list, that support.escalate uses keyed idempotency, and that the deployment supplies a compatible connection. Without those dependencies the YAML can still be parsed, but the complete workflow cannot be validated or run.

These three concepts are related but not interchangeable:

  • A Workfile document is the YAML artifact an author reads, edits, reviews, and versions. It can contain symbolic references such as helpdesk.get_ticket and project-relative calls or templates.
  • A workflow is the resolved executable definition. Resolution fixes the Workfile, connector and filter manifests, overlays, callees, templates, Standard revision, Library edition, and other behavior-affecting dependencies. Changing any of those inputs can produce a different workflow even if the root YAML is unchanged.
  • A run is one evaluation of that workflow with particular inputs, immutable run metadata, trigger data if any, action results, and other permitted nondeterministic inputs. Many runs can use one resolved workflow.

This distinction is important for review and reproducibility. A document expresses intent, resolution identifies the contracts under which that intent will execute, and a run records what happened on one occasion. Editing a source file does not retroactively change the definition used by a run already in progress. Likewise, retrying one action is part of the same logical run and does not create a new binding or a new workflow definition.

Every run receives a run binding. It includes a unique id, a recorded start time, dry-run status, and the parent run id when invoked through wf.call. Workflow inputs appear under inputs. Trigger payloads and step results become additional bindings only in the scopes defined for them.

A step is one named unit of work. Its outer key is the step id, and its body identifies either a Workfile construct or a connector action. For example:

- customer:
crm.lookup:
email: "{{ inputs.email | lower }}"

Here customer is the step id, crm.lookup is the action, and email is an action argument. If the action succeeds and its manifest declares an output, later steps can read paths such as customer.id. If the action has no output, the step still has an outcome and a stable step path but creates no readable value.

Bindings are single-assignment. A later step cannot replace customer, and a nested step cannot shadow it. Sequential steps can read earlier bindings, while a forward reference is invalid. Nested constructs create scopes so that repeated names in separate iterations or branches remain distinct. The construct’s own binding is the route by which an enclosing scope observes retained nested results.

Actions are the effectful boundary of ordinary Workfiles. Before dispatch, the executor evaluates all arguments and validates the complete argument map against the resolved manifest. The manifest also tells the executor whether an action reads or writes, how definite failures are classified, whether retry is appropriate, whether a repeated request is safe, and what result shape successful execution promises. The workflow selects and composes actions; it does not implement them.

Step modifiers express policies that apply to one invocation. when can skip a step, timeout bounds it, retry refines an action’s retry schedule, and idempotency_key supplies the stable key required by a keyed-idempotency action. Durable recovery modifiers belong to the Durable Execution Profile. Not every modifier applies to every body kind; Step Modifiers lists the permitted combinations.

on_unknown decides how an ambiguous attempt is treated; catch handles eligible failure codes; on_fail decides whether an unhandled step failure aborts the run or continues with a null binding. Omitting on_fail propagates failure to enclosing policy. on_iteration_fail and on_branch_fail control failed iterations and branches; the construct’s own on_fail still applies if the construct fails.

Workfile uses a small set of explicit constructs rather than general-purpose statements:

  • when conditionally skips one step. A conditional expression chooses between two values without creating a scope.
  • wf.route selects a branch by equality against literal case labels.
  • wf.choose evaluates ordered boolean guards and runs the first matching arm.
  • wf.for_each evaluates a finite list or supported action source and creates one isolated scope per item. Its optional max can bound the number of items, while its concurrency, failure, and retention settings control execution and the retained result.
  • wf.parallel creates named sibling branches that can run concurrently. Branches can read their enclosing scope but cannot communicate through timing or read one another before the join.
  • wf.wait delays continuation until a duration or timestamp.
  • wf.repeat performs a bounded sequence of attempts separated by waits.
  • wf.wait_for, in the Durable Execution Profile, suspends for a correlated external event or timeout.
  • wf.call invokes another statically resolved Workfile with an explicit input projection and binds its declared outputs.
  • wf.stop ends a run early with a successful result.
  • wf.fail ends a run early with an author-selected failure.

Control-flow constructs preserve structure. A route result is the selected branch scope, a parallel result contains named branch scopes, and an iteration result contains a summary and any retained scopes. Workfile does not flatten all intermediate values into a mutable global environment. That choice makes paths, lifetimes, and concurrency boundaries inspectable before execution.

In the Stateful Workflow Profile, a state’s on map declares subscriptions, timeout contains after and a transition, and transition route / cases / else select transitions. These shapes belong to state declarations; step timeout remains a duration and wf.route cases remain step lists.

Concurrency can change the order in which independent operations contact external systems, but it does not change their identities or the order in which their results are aggregated. Sequential steps execute in declaration order, and wf.for_each with concurrency: 1 processes items in source order. Authors who require external side effects to occur in a particular order must express that order through these constructs; a validator does not infer ordering constraints from apparent intent.

Workfile Core defines common document, validation, and execution semantics. Capabilities identify separately claimed features; wf.composition is required of every Core Executor, while the others are optional. Profiles add coordinated requirements for a particular execution model. Neither category is a conformance level or an ordering of importance.

The optional capabilities cover scheduled, external-event, and polling initiation; template rendering; action-source iteration; and agent execution. Each is independently claimable subject to its prerequisites. The optional profiles cover stateful workflows, durable execution, and replay. A profile can require capabilities or other profiles without changing the requirements it extends.

A workflow does not need to list required capabilities or profiles separately. Validation infers them from its syntax and resolved dependencies. This prevents declarations from drifting away from the features actually used. It also lets an implementation support, for example, external-event initiation without claiming polling, or execution without claiming durable recovery.

Structural activation matters even for paths that seem unlikely to execute. Target support covers the complete definition, not merely the branch expected for today’s inputs. A valid workflow that needs an unclaimed capability or profile is unsupported by that target; it is not reinterpreted with weaker behavior.

Core execution is the functionality an author can assume across Core Executors, including composition, the canonical time.now action, and dry runs. A capability can identify a structurally detectable dependency or an explicitly claimed operational surface such as run-record inspection. Implementation cost alone is neither necessary nor sufficient grounds for placing a feature inside or outside Core.

This informative overview summarizes the normative classifications and phases defined under Static Validation.

Validation occurs at three levels. Document validation checks the source without external dependencies. Resolved validation checks the complete workflow against the exact manifests, packages, templates, callees, and other dependencies it uses. Deployment validation determines whether a target environment can run that valid resolved workflow.

From the document alone, a validator can check serialization, closed map shapes, names, locally visible bindings, expression syntax, construct structure, and many static type relationships. After dependencies are resolved, it can also check that actions and filters exist, arguments match their manifests, output paths are declared, templates use supplied variables, calls form an acyclic bounded graph, namespaces are legitimate, and capability or profile activation is complete.

Finally, validation against a target deployment determines support: whether the target implements the required profiles and capabilities, can provide the pinned dependencies and time-zone data, and has compatible connections and applicable resource limits. Missing deployment support does not make the Workfile malformed. It means that this otherwise valid resolved workflow cannot run on that target.

Before activating a trigger, starting a run, or dispatching an action, an executor successfully parses and validates the document, resolves and validates its dependencies, and confirms support from the target deployment. Validation and execution use the same resolved dependencies and deployment configuration; otherwise, the earlier type and operational checks no longer apply. Values that cannot be checked statically—for example, data declared as json—are still validated at runtime, but executors do not defer conditions that can be checked before execution.