Automate with Workflows
This tutorial builds an approval workflow for a documents collection. Documents move from draft → review → approved (or rejected).
What you’ll build
- A
documentscollection wired up with the required workflow fields - A workflow definition and assignment linking it to the collection
- An extension that sends an email notification on state change
- API calls to trigger transitions and query history
Create the documents collection
In Data Model, create a collection called documents with these fields:
| Field | Type | Notes |
|---|---|---|
title | string | Required |
body | text | |
status | string | Default: draft |
reviewer_id | M2O → daas_users |
Then add the two fields required by the workflow engine. If you are creating this collection fresh via the collection wizard, enable both switches under System Fields → Workflow Fields:
| Switch | Field created | Purpose |
|---|---|---|
| Workflow Instance | workflow_instance | M2O → daas_wf_instance, stores the active workflow instance ID |
| Workflow State | workflow_state | xtr-interface-workflow interface, stores the current state name |
If you are adding workflows to an existing collection, add the two fields manually via Data Model → [Your Collection] → New Field using the same types above.
Mark both as read-only in their field metadata — users should not edit them directly.
Define the workflow
Create the workflow definition via the API. Replace <editor-policy-uuid> with the UUID of the policy assigned to your Editor role:
curl -X POST https://your-domain.com/api/workflows \
-H "Authorization: Bearer <admin_token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Document Approval",
"workflow_json": {
"initial_state": "draft",
"states": [
{
"name": "draft",
"commands": [
{ "name": "Submit for Review", "next_state": "review", "policies": [], "actions": [] }
],
"isEndState": false
},
{
"name": "review",
"commands": [
{
"name": "Approve",
"next_state": "approved",
"policies": ["<editor-policy-uuid>"],
"actions": [
{ "name": "Notify on approve", "event_name": "xtr.docs.approved", "parameters": {} }
]
},
{
"name": "Reject",
"next_state": "rejected",
"policies": ["<editor-policy-uuid>"],
"actions": []
}
],
"isEndState": false
},
{
"name": "approved",
"commands": [],
"isEndState": true
},
{
"name": "rejected",
"commands": [
{ "name": "Revise", "next_state": "draft", "policies": [], "actions": [] }
],
"isEndState": false
}
]
}
}'Commands are embedded inside each state — there is no top-level transitions array. The policies field takes an array of policy UUIDs (not role names). Leave it empty ([]) to allow any authenticated user to trigger that command.
You can also build simpler workflows visually: go to Workflow Definitions under Automation in the sidebar, click Add Workflow, enter a name, then use the State Diagram canvas to add states and commands through the UI modals.
Note the id returned in the response — you’ll need it as <workflow-definition-uuid> in the next step.
Then create a Workflow Assignment to link the definition to the documents collection:
curl -X POST https://your-domain.com/api/workflow-assignments \
-H "Authorization: Bearer <admin_token>" \
-H "Content-Type: application/json" \
-d '{
"collection": "documents",
"workflow": "<workflow-definition-uuid>",
"filter_rule": {}
}'An empty filter_rule matches all items in the collection.
Add an automated notification action
Create a runtime extension to handle the xtr.docs.approved action event. Runtime extensions are stored in the database and hot-reloaded — no deployment or Docker access required.
curl -X POST https://your-domain.com/api/items/daas_extensions \
-H "Authorization: Bearer <admin_token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Notify on approval",
"event": "xtr.docs.approved",
"type": "action",
"status": "active",
"code": "const { workflow_instance } = meta.payload[0];\nconsole.log(\"Document approved:\", workflow_instance);"
}'The log output appears immediately in Logs under the System section of the sidebar — use that to verify the action fires when you trigger the Approve command in the next step.
Once you are ready to send real notifications, swap the console.log line for one of these in the code field:
Outbound webhook (requires EXTENSION_ALLOWED_DOMAINS to include the target domain):
await context.services.fetch('https://hooks.example.com/approval', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ workflow_instance }),
});Email (requires SMTP environment variables to be configured):
await context.services.mail({
to: 'reviewer@example.com',
subject: 'Document approved',
text: `Workflow instance ${workflow_instance} has been approved.`,
});Trigger a transition via the API
First, fetch the workflow_instance ID from the document:
curl "https://your-domain.com/api/items/documents/1?fields=workflow_instance" \
-H "Authorization: Bearer <token>"Then post the transition using the instance ID and the command name:
curl -X POST https://your-domain.com/api/workflow/transition \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"workflowInstanceId": "<workflow-instance-uuid>",
"commandName": "Submit for Review"
}'The API validates that the command exists in the current state and that the user holds at least one required policy.
Track the workflow history
Query the transition history for a specific workflow instance:
curl "https://your-domain.com/api/workflow-instances/<instance-uuid>/history" \
-H "Authorization: Bearer <token>"Next Steps
- Add more policies to commands for multi-level approval
- Combine with Extensions to run custom code on state change
- Use Cron Jobs to send daily digest emails of pending reviews