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.
A Complete Workfile
Section titled “A Complete Workfile”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: 1name: triage_ticketowner: support_operationsversion: 3
inputs: ticket_id: type: string required: true urgent: type: bool default: falsesteps: - 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:
inputsdescribes the invocation boundary. The executor checks supplied values and applies defaults before the first step begins.- Each item in
stepshas one id, such asticketortriage. A successful value-producing step makes that id readable by later steps. support.get_ticketand the other dotted non-wfkeys are connector actions. Their argument and result shapes come from the resolvedsupportmanifest.- The idempotency key is a domain value, so a second run for the same ticket does not escalate twice;
run.idwould 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.choosecreates one selected branch scope. The branch-localescalationandnotebindings are reachable throughtriage, subject to the branch-binding rules.wf.for_eachcreates isolated iteration scopes. Its explicitmaxbounds work caused by the input, whileconcurrencypermits up to four independent checks at once.count,succeeded, andfailedremain available in every retention mode.retain: failureskeeps failed scopes underfailures; the defaultretain: allkeeps every scope underitems. Changing retention preserves summary access, but references to discarded scopes become invalid.wf.valuebuilds a new value without causing an external effect.outputsselects 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: abortterminates the run even underon_fail: continue,on_iteration_fail: continue, oron_branch_fail: continue. Chooseon_unknown: failto passflow.action_ambiguousintocatch,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_atis the fixed instant at which the run actually began; a workflow observes a later current instant through the canonicaltime.nowaction.
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.
Documents, Workflows, and Runs
Section titled “Documents, Workflows, and Runs”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_ticketand 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.
Steps, Bindings, and Actions
Section titled “Steps, Bindings, and Actions”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.
Control Flow
Section titled “Control Flow”Workfile uses a small set of explicit constructs rather than general-purpose statements:
whenconditionally skips one step. A conditional expression chooses between two values without creating a scope.wf.routeselects a branch by equality against literal case labels.wf.chooseevaluates ordered boolean guards and runs the first matching arm.wf.for_eachevaluates a finite list or supported action source and creates one isolated scope per item. Its optionalmaxcan bound the number of items, while its concurrency, failure, and retention settings control execution and the retained result.wf.parallelcreates 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.waitdelays continuation until a duration or timestamp.wf.repeatperforms 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.callinvokes another statically resolved Workfile with an explicit input projection and binds its declared outputs.wf.stopends a run early with a successful result.wf.failends 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.
Optional Capabilities and Profiles
Section titled “Optional Capabilities and Profiles”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.
Validation Before Execution
Section titled “Validation Before Execution”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.