- name
- flow-automation
- description
- Flow automation — triggers, operations, data chains, event hooks, webhooks, schedules. This skill should be used when the user asks to create automation flows, set up triggers, configure operations, build event-driven workflows, schedule tasks, or trigger flows programmatically in Directus.
# 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