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.
Authoring process
Section titled “Authoring process”Start with the business outcome, not the element list.
- Name the outcome:
CreateProjectTask,SendDocumentReminder,ExportReport,ProjectAccessPolicy. - Choose the trigger: button, field change, create/update/delete event, cron, policy, or another workflow.
- List the inputs: current resource, modal fields, request data, selected rows, current user, or schedule payload.
- List the outputs: saved resources, response data, client field updates, files, notifications, or policy constraints.
- Sketch the graph: load, branch, create, set, save, respond.
- Decide sync vs async. UI responses stay sync; long-running jobs become async.
- Add tests for the externally visible behavior.
Pattern: create and save
Section titled “Pattern: create and save”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.
Pattern: modal, confirm, save, notify
Section titled “Pattern: modal, confirm, save, notify”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." } }}Pattern: load, transform, response
Section titled “Pattern: load, transform, response”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.
Pattern: async job with progress
Section titled “Pattern: async job with progress”Long work should not run inside the browser request. A common pattern is:
- A sync webhook validates the request.
action/runstarts an async workflow.- The async workflow reports status with
cast/progress. - 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.
Pattern: policy filter
Section titled “Pattern: policy filter”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.
Pattern: export or PDF flow
Section titled “Pattern: export or PDF flow”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.
Pattern: run another workflow
Section titled “Pattern: run another workflow”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.
Checklist
Section titled “Checklist”- The workflow name describes a business outcome.
- The
entryelement 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 withop/set, and persisted withaction/save. - Examples, labels, and notifications do not expose customer, company, domain, or infrastructure details.
- Behavior is covered by workflow tests where the outcome matters.