Activities and actions
An activity is work scoped to one stage visit. Actions resolve it, write instance state, and queue effects, fired by a caller or automatically by the engine.
Early access
Workflows is in early access, built in public. Read How early access works before you rely on it.
An activity is a unit of work that belongs to one stage visit. Its actions are the named responses available while that work is active. A caller fires an action with fireAction. An action that declares a when condition is an automated trigger the engine fires itself. For how a stage visit begins and ends, see Definitions, instances, and stages.
Firing an action is one all-or-nothing commit: its operations, the activity status it sets, and its history rows land together or not at all. An action never moves the instance itself. Transitions read the state the commit produced and decide the move. Effects queued by the action are saved in that same commit. They only reach outside the engine when a runtime drains them.
Define an activity
An activity belongs to a stage’s activities array. Declare the work it represents, the condition that decides whether it applies to a stage visit, and the actions available while it is active:
defineActivity({
name: 'review',
title: 'Review article',
filter: '$fields.needsReview',
actions: [defineAction({name: 'approve', status: 'done'})],
})Existence and readiness: filter and requirements
An activity’s filter decides whether the activity exists for a stage visit. The engine evaluates it once, at stage entry. A definite false records the activity as skipped before it ever starts, and $allActivitiesDone ignores it. If the engine cannot decide the filter, the activity stays in scope. That way a missing value never opens the gate the activity was protecting.
Requirements keep actions disabled until an activity is ready. This example requires a person in the editable reviewer field before they can approve. Its assignee field needs Workflows 0.32 and reader model 9; follow Upgrade Workflows packages before deploying it.
defineActivity({
name: 'review',
fields: [{type: 'assignee', name: 'reviewer', editable: true}],
requirements: [
{
type: 'groq',
name: 'assigned-reviewer',
title: 'Assigned reviewer required',
query: "count($fields.reviewer[@.type == 'user']) > 0",
},
],
actions: [defineAction({name: 'approve', filter: '$assigned', status: 'done'})],
})filter and requirements both hold conditions, but they answer different questions.
filter
Condition
Controls whether the activity belongs to a stage visit. An excluded activity is recorded as skipped and does not block completion.
requirements
GroqRequirement[]
Ordered named readiness gates. Unmet requirements keep the activity visible but block caller-fired actions.
A disabled action is advice, not enforcement. The verdict exists so an interface can grey out the right control and explain why it is unavailable. Anyone holding a write token can reach the Content Lake directly and skip the engine. The Content Lake is the only enforcement point, so a rule that must actually hold lives in dataset access control.
An in-scope activity is active from stage entry. It stays active until an action resolves it as done, skipped, or failed. Resolving an activity never moves the instance; a transition does that.
Define actions
Actions belong to an activity’s actions array. Each one names a response that can be fired while that activity is active.
Omit when and the action is caller-fired: any caller holding a token can invoke it with fireAction. Declare when and the action becomes an automated trigger that the engine fires itself, and fireAction can never invoke it.
An action’s filter decides whether the action exists for the caller. A failed filter produces filter-failed, so a surface hides the action. When $assigned hides an action, its evaluation can include holderGate so the activity can explain who holds the work. On an automated trigger, filter decides whether the automation exists and when decides when it fires.
The first action records who approved, resolves the review activity, and queues a notification. The second declares when, so the engine fires it instead of a caller:
defineAction({
name: 'approve',
ops: [
{type: 'field.set', target: {field: 'approval'}, value: {type: 'actor'}},
],
effects: [
{
name: 'notify-reviewer',
bindings: {to: '$fields.reviewer'},
input: {body: 'Your article was approved'},
},
],
status: 'done',
})
defineAction({
name: 'escalate',
when: '$fields.dueDate < $now',
effects: [{name: 'notify-editor', bindings: {to: '$fields.editor'}}],
})Caller-fired actions and automated triggers
Without when
Caller-fired action
A caller invokes it through fireAction. A UI may present it as a control.
With when
Automated trigger
The engine fires it automatically when its condition becomes true.
The engine fires each automated trigger at most once per stage visit, in declaration order: activities in the order the stage declares them, then each activity’s actions in order. Re-entering the stage starts a new visit, and every trigger becomes eligible again.
Nothing fires on a timer. The engine is a library, not a service, so an automated trigger is only ever evaluated inside a call your own code makes. A trigger whose when compares a deadline to $now fires on the first tick after the clock crosses it, and something in your runtime has to make that call.
Resolve an activity
An action resolves its activity by declaring a status. The engine never assumes one, so an action with no status leaves its activity active and the stage visibly stalls. Use status for the outcome of the work, never for the business decision:
done: the work completed, including approve, decline, send back, and hold.skipped: the work did not apply.failed: the work could not complete.
For a business decision, resolve the activity as done and write the decision into a field that a transition condition reads. Routing lives in the transition, never in the status.
What an action can do
One action firing can combine immediate operations on instance state, queued effects, spawned subworkflows, and a status that resolves the firing activity.
Preview an action’s flow consequence
evaluate previews what firing a caller-fired action would do to the flow right now. The engine replays the fire in memory against the current state, including any automated triggers it would set off and the transition that would be selected next. Read firing on the action’s evaluation to place or label a control without duplicating transition logic in your interface.
const evaluation = await engine.evaluate({instanceId})
const approve = evaluation.currentStage.activities
.flatMap(({actions}) => actions)
.find(({action}) => action.name === 'approve')
if (approve?.firing?.exitsStage) {
console.log(`Approve exits through ${approve.firing.transition}`)
}exitsStage
boolean
True when the replayed action and cascade leave the current stage.
transition
string | undefined
The selected transition name when exitsStage is true.
The preview is advisory. It describes the state as projected right now, and it is not a promise about the commit. The engine leaves firing out when an accurate replay would need something the projection does not hold: caller parameters, a spawn whose fan-out comes from the dataset, a terminal instance, or a stage visit that no longer matches the pinned definition. It is also left out on automated triggers, which are never a caller's to fire.
Condition variables in activities and actions
Activity and action conditions are GROQ, evaluated over the instance’s in-memory snapshot using the variables Conditions defines. Which variables are available depends on where the condition sits. Activity requirements and a caller-fired action’s filter run with a caller, so they also bind $actor, $assigned, $can, and $attributes. Activity filters and an automated trigger’s when and filter run during the cascade with no caller, so deploy rejects a condition that reads a caller variable there.
All sites
Instance variables
Workflow state such as $fields, $stage, $activities, and $context.
Caller-bound sites
$actor, $assigned, $can, $attributes
Available to activity requirements and caller-fired action filters.
Cascade sites
No caller variables
Activity filters and triggered action conditions run without a caller.
Reference
Activity declaration
This is the object passed to defineActivity(...) in a stage’s activities array.
name
string
Required stable activity name.
title
string
Optional display label.
description
string
Optional explanation.
semantics
Semantic[]
Optional advisory signal or custom meaning. Evaluation preserves it; execution does not change.
groups
Group[]
Groups declared for this activity’s fields and actions.
group
string | string[]
Groups this activity joins.
target
ManualTarget
Optional deep link for work performed outside Workflows.
filter
Condition
Existence condition evaluated at stage entry.
requirements
GroqRequirement[]
Ordered named readiness gates; all must pass before caller-fired actions are executable.
actions
Action[]
Actions that can resolve work or change instance state.
fields
FieldEntry[]
Activity-scoped fields resolved at stage entry.
Action declaration
This is the object passed to defineAction(...) in an activity’s actions array.
name
string
Required stable action name.
title
string
Optional display label.
description
string
Optional explanation.
group
string | string[]
Groups this action joins.
when
Condition
When present, makes this a cascade-fired trigger; otherwise callers use
fireAction.filter
Condition
Existence condition for the action.
roles
string[]
Advisory role requirement; trigger roles also pin cascade execution.
params
ActionParam[]
Caller-supplied values validated before the action commits.
ops
Operation[]
Synchronous instance mutations committed with the firing.
effects
Effect[]
External work queued by the firing.
spawn
Subworkflows
Child workflows spawned by the firing.
status
done | skipped | failed
Authoring shorthand for a final
status.seton the firing activity.semantics
ActionSemantic[]
Optional advisory decision, signal, or custom meaning. Decision values are action-only; semantics do not change execution.
Visiting agent?
Workflows includes an MCP server for inspecting, operating, authoring, validating, and deploying workflows. Ask your human to set up the MCP server.