Skip to content

Data Flow

Workflow data moves through ports and context variables. The graph decides where execution goes; set, in, and recipes decide what data each element sees.

Store element output with set.

{
"name": "loadProject",
"type": "source/entities",
"options": {
"module": "projects",
"first": true
},
"set": {
"project": "default:"
}
}

After this element runs, later recipes can read $project.

Common bindings:

BindingMeaning
default:Data emitted from the default output port.
input:Request or event input payload.
confirm:Data confirmed by an action/modal.
error:Error output where an element supports it.
event:Event stream payload from elements that emit progress or service events.
:selectorSpecial event data such as the field selector in on_change flows.

Use in to pass a context value to a specific input port.

{
"name": "saveProject",
"type": "action/save",
"in": {
"default": "$project"
}
}

Without in, many elements receive the data that arrived at their connected input port. Use explicit in bindings when a workflow has multiple resources in context and the target element must operate on one of them.

Recipes calculate values from the current context.

{
"name": "prepareSummary",
"type": "op/recipe",
"options": {
"recipe": "{\"title\": $project.name, \"task_count\": count($tasks)}"
},
"set": {
"summary": "default:"
}
}

Use recipes for compact transformations. If the expression becomes difficult to read, split it into several named op/recipe or op/set elements.

Read more in the Recipes reference.

op/set changes the workflow data object. It is commonly used after source/new or after loading an existing resource.

{
"name": "setTaskFields",
"type": "op/set",
"options": {
"copy": "$taskData",
"recipes": {
"project": "$project",
"status": "\"open\"",
"assigned_to": "user()"
}
}
}

Use copy when user input should become the base object. Use recipes for fields that must be calculated or controlled by the workflow.

Use flow/each when every resource in an array needs the same operation.

{
"name": "forEachTask",
"type": "flow/each",
"to": {
"default": {
"setTaskStatus": ["default"]
},
"empty": {
"notifyNoTasks": ["default"]
}
}
}

Keep loop bodies small. If each item needs a complex process, move that process into a separate workflow and call it with action/run.

Field events can provide a selector that points to the changed part of the form. Store it and use it when setting values back on the client.

{
"name": "selectChangedItem",
"type": "op/selector",
"set": {
"fieldSelector": ":selector"
}
}
{
"name": "setClientValue",
"type": "action/set",
"options": {
"copy": null,
"add": [],
"recipes": {
"description": "$selectedTemplate.description"
},
"properties": {
"validateMessage": {
"${fieldSelector}.description": "null"
}
}
}
}

Use this pattern for list items so the workflow updates the changed row instead of the entire form.

  • Store important intermediate values with clear names.
  • Use explicit in bindings when more than one resource is in context.
  • Keep recipes short and named by intent.
  • Handle empty arrays before looping.
  • Do not pass private service responses to the client unless the workflow intentionally sanitizes them.