- name
- flow-automation
- description
- Build and run Directus flows, covering event hooks, schedules, webhook and manual triggers, operations such as condition, request, read and update data, and the data chain that passes values between operations. Use it for any Directus question about creating, wiring, scheduling or triggering an automation flow, in any language. For a flow that does not run or a condition that never matches use troubleshoot.
# Flow Automation
Complete reference for the `flows`, `operations`, and `trigger-flow` MCP tools.
## Concepts
- **Flow** = Trigger + Chain of Operations
- Each flow has ONE trigger and a series of operations forming a data chain
- Operations connect via `resolve` (success) and `reject` (failure) paths
- Data passes through the chain using `{{ operation_key }}` variables
## Trigger Types
| Trigger | When It Fires | Key Options |
|---------|---------------|-------------|
| `event` | On item CRUD (hook) | `type` (filter/action), `scope` (items.create, items.update, items.delete), `collections` |
| `webhook` | HTTP request to flow URL | `method` (GET/POST), `async` |
| `schedule` | Cron expression | `cron` (e.g., `"0 9 * * 1"` = Monday 9am) |
| `operation` | Called by another flow | — |
| `manual` | User triggers from app | `collections`, `requireSelection`, `fields` |
## Creating a Flow
```text
Tool: flows
Input: {
"action": "create",
"data": {
"name": "Notify on New Post",
"trigger": "event",
"status": "active",
"accountability": "all",
"icon": "notifications",
"color": "#FF9800",
"description": "Send notification when a new post is created",
"options": {
"type": "action",
"scope": ["items.create"],
"collections": ["posts"]
}
}
}
```
### Trigger Options by Type
**Event trigger:**
```jsonc
"options": {
"type": "action",
"scope": ["items.create", "items.update"],
"collections": ["posts"]
}
```
- `type: "filter"` — runs before the operation (can block it)
- `type: "action"` — runs after the operation
**Schedule trigger:**
```jsonc
"options": {
"cron": "0 */6 * * *"
}
```
**Manual trigger:**
```jsonc
"options": {
"collections": ["posts"],
"requireSelection": true,
"fields": [
{ "field": "reason", "type": "string", "name": "Reason" }
]
}
```
**Webhook trigger:**
```jsonc
"options": {
"method": "POST",
"async": false
}
```
## Operation Types
| Type | Description | Key Options |
|------|-------------|-------------|
| `condition` | If/else branching | `filter` (condition rules) |
| `item-create` | Create items | `collection`, `payload` |
| `item-read` | Read items | `collection`, `query` |
| `item-update` | Update items | `collection`, `payload`, `key` or `query` (see below) |
| `item-delete` | Delete items | `collection`, `key` or `query` (see below) |
| `mail` | Send email | `to`, `subject`, `body` |
| `notification` | In-app notification | `recipient`, `subject`, `message` |
| `request` | HTTP request | `url`, `method`, `headers`, `body` |
| `sleep` | Wait/delay | `milliseconds` |
| `log` | Log to console | `message` |
| `exec` | Run custom code | `code` |
| `transform` | Transform data | `json` |
| `trigger` | Chain to another flow | `flow` |
| `json-web-token` | Sign, verify or decode a JWT | `operation` (`sign`/`verify`/`decode`), `payload`, `token`, `secret`, `options` |
| `throw-error` | Fail the flow with a custom error | `code`, `status`, `message` |
### Targeting in `item-update` and `item-delete` (Directus 12.3.0+)
Target items with **either** `key` (one primary key or an array) **or** `query`, never both:
- Empty or missing `key` and `query` (including `"query": {}`) return `null` and change nothing. Before 12.3.0 they affected every item in the collection.
- To update or delete **all** items, say so explicitly with `"query": { "limit": -1 }`.
- `key` together with `query` fails the operation with `INVALID_PAYLOAD` ("Cannot use both key and query"). `item-update` with a batch (array) payload together with `key` or `query` throws too, because a batch payload carries its own keys.
- 12.4.0+: update and delete by `query` apply the **read** permissions of the operation's accountability. The role needs read on the primary key, and only readable items are touched. Fields used in `filter` and `sort` must be readable.
- Both operations also take `emitEvents` (fire hooks and flows for the change) and `permissions` (`$trigger`, `$full`, `$public` or a role UUID) to choose whose permissions apply.
```text
Tool: operations
Input: {
"action": "create",
"data": {
"flow": "FLOW_UUID",
"key": "archive_old_drafts",
"type": "item-update",
"name": "Archive old drafts",
"options": {
"collection": "posts",
"payload": { "status": "archived" },
"query": {
"filter": { "date_created": { "_lt": "$NOW(-90 days)" }, "status": { "_eq": "draft" } },
"limit": -1
}
},
"position_x": 40,
"position_y": 1
}
}
```
A `query` without a `limit` is capped at `QUERY_LIMIT_DEFAULT` (100 unless configured), so a bulk change over more matches needs `"limit": -1` as above (bounded by `QUERY_LIMIT_MAX` when that is set). With the `filter` this still targets only old drafts. A bare `{"limit": -1}` with no `filter` means the whole collection, so use it only on purpose.
## Creating Operations
```text
Tool: operations
Input: {
"action": "create",
"data": {
"flow": "flow-uuid-here",
"key": "check_status",
"type": "condition",
"name": "Check if Published",
"options": {
"filter": {
"$trigger": {
"payload": {
"status": { "_eq": "published" }
}
}
}
},
"position_x": 20,
"position_y": 1,
"resolve": "next-op-uuid-or-null",
"reject": null
}
}
```
## Connecting Operations
Operations form a chain through `resolve` and `reject` UUIDs:
1. Create all operations first (with `resolve: null`, `reject: null`)
2. Update each operation with the correct `resolve`/`reject` UUIDs
3. Update the flow with `operation: "first-op-uuid"` to set the entry point
```text
Tool: flows
Input: {
"action": "update",
"key": "flow-uuid",
"data": { "operation": "first-operation-uuid" }
}
```
## Data Chain Variables
Access data from triggers and previous operations:
| Variable | Source |
|----------|--------|
| `{{ $trigger }}` | Trigger data (payload, keys, collection) |
| `{{ $trigger.payload }}` | Item data that triggered the flow |
| `{{ $trigger.keys }}` | Primary keys of items |
| `{{ $trigger.collection }}` | Collection name |
| `{{ $trigger.body }}` | Webhook/manual trigger body |
| `{{ operation_key }}` | Output of a specific operation (use the `key` you set) |
| `{{ $accountability }}` | User who triggered the flow |
### Critical Syntax Rules
1. **Condition filters** — Use nested objects, NOT dot notation:
```jsonc
// CORRECT
{ "$trigger": { "payload": { "status": { "_eq": "published" } } } }
// WRONG
{ "$trigger.payload.status": { "_eq": "published" } }
```
2. **Request headers** — Array of `{header, value}` objects, NOT key-value:
```jsonc
// CORRECT
"headers": [
{ "header": "Content-Type", "value": "application/json" },
{ "header": "Authorization", "value": "Bearer token" }
]
// WRONG
"headers": { "Content-Type": "application/json" }
```
3. **Request body** — Stringified JSON, NOT native objects:
```jsonc
// CORRECT
"body": "{\"message\": \"{{ $trigger.payload.title }}\"}"
// WRONG
"body": { "message": "{{ $trigger.payload.title }}" }
```
4. **Data chain references** — Use explicit operation keys:
```jsonc
// CORRECT (using operation key)
"{{ read_user.email }}"
// AVOID
"{{ $last }}"
```
## Complete Flow Example
### "Send Slack Notification on New Published Post"
**Step 1: Create the flow:**
```text
Tool: flows
Input: {
"action": "create",
"data": {
"name": "Notify Slack on Publish",
"trigger": "event",
"status": "active",
"options": {
"type": "action",
"scope": ["items.create", "items.update"],
"collections": ["posts"]
}
}
}
```
**Step 2: Create condition operation:**
```text
Tool: operations
Input: {
"action": "create",
"data": {
"flow": "FLOW_UUID",
"key": "is_published",
"type": "condition",
"name": "Is Published?",
"options": {
"filter": {
"$trigger": {
"payload": {
"status": { "_eq": "published" }
}
}
}
},
"position_x": 20,
"position_y": 1
}
}
```
**Step 3: Create HTTP request operation:**
```text
Tool: operations
Input: {
"action": "create",
"data": {
"flow": "FLOW_UUID",
"key": "send_slack",
"type": "request",
"name": "Send to Slack",
"options": {
View on GitHub