Skip to content

Workflows

Workflows let you build business processes out of small JSON-defined elements. A good workflow reads like a process map: it starts from an event, loads or creates data, transforms it, saves or returns it, and then tells the user what happened.

This guide shows practical authoring patterns. For the underlying model, read the Workflow Overview first.

Start with the business outcome, not the element list.

  1. Name the outcome: CreateProjectTask, SendDocumentReminder, ExportReport, ProjectAccessPolicy.
  2. Choose the trigger: button, field change, create/update/delete event, cron, policy, or another workflow.
  3. List the inputs: current resource, modal fields, request data, selected rows, current user, or schedule payload.
  4. List the outputs: saved resources, response data, client field updates, files, notifications, or policy constraints.
  5. Sketch the graph: load, branch, create, set, save, respond.
  6. Decide sync vs async. UI responses stay sync; long-running jobs become async.
  7. Add tests for the externally visible behavior.

Use source/new, op/set, and action/save when a workflow creates a resource from the current context.

{
"identifier": "CreateProjectTask",
"title": "Create project task",
"async": false,
"entry": "createProjectTask",
"elements": [
{
"name": "createProjectTask",
"type": "event/webhook",
"options": {
"module": "projects",
"refresh_prototype": true
},
"set": {
"project": "default:",
"data": "input:"
},
"to": {
"default": {
"newTask": ["default"]
}
}
},
{
"name": "newTask",
"type": "source/new",
"options": {
"module": "tasks"
},
"to": {
"default": {
"setTaskFields": ["default"]
}
}
},
{
"name": "setTaskFields",
"type": "op/set",
"options": {
"recipes": {
"title": "$data.title",
"project": "$project",
"status": "\"open\""
}
},
"to": {
"default": {
"saveTask": ["default"]
}
}
},
{
"name": "saveTask",
"type": "action/save"
}
],
"events": [
{
"identifier": "createProjectTask",
"type": "webhook",
"module": "projects"
}
]
}

Keep the first version this small. Add branching, notifications, and follow-up work only when the core create path is stable.

Use a modal when the workflow needs additional user input before changing data.

{
"name": "openTaskModal",
"type": "action/modal",
"options": {
"type": "confirm",
"layout": "tasks.modal",
"checkpoint": true,
"autosave_key": "task_create"
},
"set": {
"taskData": "confirm:"
},
"to": {
"confirm": {
"newTask": ["default"]
}
}
}

Follow the modal with source/new, op/set, action/save, and action/notify. Use neutral notification text that describes the result:

{
"name": "notifyTaskCreated",
"type": "action/notify",
"options": {
"toast": {
"type": "success",
"message": "Task created",
"description": "The task was added to the project."
}
}
}

Dashboard widgets, option loaders, and lightweight API-style calls often load records, transform them with recipes, and return a response.

{
"name": "buildSummary",
"type": "op/recipe",
"options": {
"recipe": "{\"open_tasks\": count($tasks), \"project\": $project.name}"
},
"set": {
"summary": "default:"
},
"to": {
"default": {
"sendSummary": ["default"]
}
}
}
{
"name": "sendSummary",
"type": "action/response",
"in": {
"default": "$summary"
}
}

Keep these workflows synchronous because the browser is waiting for the response.

Long work should not run inside the browser request. A common pattern is:

  1. A sync webhook validates the request.
  2. action/run starts an async workflow.
  3. The async workflow reports status with cast/progress.
  4. The workflow saves the result and sends a final notification.
{
"identifier": "PublishProject",
"title": "Publish project",
"async": true,
"entry": "publishProject",
"elements": [
{
"name": "publishProject",
"type": "event/webhook",
"options": {
"module": "projects"
},
"set": {
"project": "default:"
},
"to": {
"default": {
"showProgress": ["default"]
}
}
},
{
"name": "showProgress",
"type": "cast/progress",
"options": {
"title": "Publishing project",
"total": 3,
"progress": 1,
"send_to_user": true
}
}
]
}

For details, see Async Progress and Jobs and Cast.

Policy workflows constrain which resources a user may access. They usually start with event/policy and add one or more query/where elements.

{
"identifier": "ProjectAccessPolicy",
"title": "Project access policy",
"async": false,
"entry": "projectPolicy",
"elements": [
{
"name": "projectPolicy",
"type": "event/policy",
"options": {
"module": "projects",
"roles": ["employee"],
"operations": ["read", "update"],
"allow": ["admin", "manager"],
"roleKey": "slug"
},
"to": {
"default": {
"whereAssigned": ["default"]
}
}
},
{
"name": "whereAssigned",
"type": "query/where",
"options": {
"where": [
[
{
"left": {
"type": "field",
"value": "assigned_users.id"
},
"operator": "=",
"right": {
"type": "recipe",
"value": "user().id"
}
}
]
]
}
}
]
}

See Policies and Access before writing policy workflows; small mistakes can expose too much or hide too much.

PDF export flows commonly write a file with action/export and return it to the browser with action/viewFile. The export action references Brezel views for the PDF content, header, and footer where needed.

{
"name": "exportReportPdf",
"type": "action/export",
"options": {
"type": "pdf",
"filename": "project-report.pdf",
"content": "reports.project_summary",
"store_module": "files",
"view_module": "views",
"view_data": {
".recipe": "{\"project\": $project, \"items\": $items}"
}
},
"to": {
"default": {
"viewReportPdf": ["default"]
}
}
}
{
"name": "viewReportPdf",
"type": "action/viewFile"
}

Keep direct file responses synchronous because action/viewFile returns an HTTP response. For large batch exports, start an async workflow, store the generated file, and notify the user when it is ready. See PDF Export Workflow for the focused version of this pattern.

Use action/run when one workflow coordinates another workflow. This keeps the caller small and lets the called workflow be tested independently.

{
"name": "startPublishJob",
"type": "action/run",
"options": {
"workflow": "PublishProject",
"async": true,
"scope": true,
"input": {
"project": "$project"
}
}
}

Set scope to true when the child workflow should receive an explicit context. Pass only the variables it needs through input. Use in when the called workflow should receive a specific default input resource.

  • The workflow name describes a business outcome.
  • The entry element exists and matches the intended trigger.
  • Every branch that can fail or be empty has an intentional path.
  • UI response workflows are synchronous.
  • Long jobs are asynchronous and report progress.
  • Saved resources are created with source/new, filled with op/set, and persisted with action/save.
  • Examples, labels, and notifications do not expose customer, company, domain, or infrastructure details.
  • Behavior is covered by workflow tests where the outcome matters.