Workflow Activities
Activities are the units of work within workflows. Each activity is an autonomous LLM task with full tool access. Activities execute sequentially -- each activity's output becomes context for the next. This page covers activity structure, execution model, budgets, error handling, and a complete working example.
Workflow Binding
Each entry in the workflows map binds activities to a trigger:
{
"workflows": {
"morning-briefing": {
"trigger": { "type": "schedule", "cron": "0 7 * * *" },
"description": "Daily morning briefing",
"activities": [
{
"id": "gather",
"intent": "Gather today's priorities from calendar and email",
"skills": ["@nebo/skills/briefing-writer"],
"steps": ["Check calendar for today", "Scan inbox for urgent items", "Compose briefing"],
"token_budget": { "max": 4096 }
},
{
"id": "deliver",
"intent": "Send the briefing to the user",
"steps": ["Format as concise bullet points", "Post to chat"],
"token_budget": { "max": 1024 }
}
],
"budget": { "total_per_run": 6000 },
"emit": "briefing.ready"
}
}
}Binding Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
ref | string | no | -- | External workflow qualified name (@org/workflows/name@version). Optional when using inline activities. |
trigger | object | yes | -- | When this workflow runs (see Triggers) |
description | string | no | "" | Human-readable description |
inputs | map | no | {} | Default inputs passed to the workflow on trigger |
activities | array | no | [] | Inline activity definitions. When present, the workflow runs inline -- no external ref needed. |
budget | object | no | {} | Token budget constraints (total_per_run) |
emit | string | no | -- | Event name to emit on completion. By convention, prefix with the agent slug (e.g., executive-assistant.briefing.ready) to avoid cross-agent collisions. |
Activities
Activities define the steps an agent takes when a workflow fires. Each activity is an autonomous LLM task with full tool access. Activities execute sequentially -- each activity's output becomes context for the next.
Activity Fields
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Unique activity ID within the binding |
intent | string | yes | Task description -- what the LLM should accomplish |
steps | string[] | no | Step-by-step hints for the LLM |
skills | string[] | no | Skill references available to this activity (@org/skills/name) |
mcps | string[] | no | MCP server slugs available to this activity |
model | string | no | Model override ("sonnet", "haiku", "opus") |
token_budget | object | no | Per-activity token limit: { "max": 4096 } |
on_error | object | no | Error handling: { "retry": 1, "fallback": "skip" } |
min_iterations | number | no | Force LLM to continue even if it wants to stop early |
How Activities Execute
The execution model for each activity follows a predictable sequence:
- System prompt is constructed from: execution rules, skills, available tools, intent, steps, inputs, and prior activity results
- Agentic loop runs (up to 50 iterations): LLM streams, tool calls, results, repeat
- Context chaining: Each activity's output is appended as
[Activity '{id}' result]: {text}and passed to the next activity - Branch termination: If an activity produces empty output (empty string), its branch stops -- no downstream activities execute. This follows n8n-style semantics where empty output signals "nothing to do."
- Completion: Activity ends when LLM produces text without tool calls (or budget exhausted)
Activity Results
Each completed activity records the following fields. Results are stored per run and available in system events and the workflow run API response.
| Field | Type | Description |
|---|---|---|
started_at | timestamp | When the activity began executing |
completed_at | timestamp | When the activity finished |
input_tokens | number | Token usage for input (prompt) |
output_tokens | number | Token usage for output (completion) |
error_pattern | string | Error pattern tracked for circuit breaker (if failed) |
failure_reason | string | Human-readable failure reason (if failed) |
Built-in Tools
Two tools are always available inside activities, regardless of what skills or plugins are configured:
| Tool | Purpose |
|---|---|
emit | Emit an event into the EventBus. Other agents can subscribe to it. |
exit | Stop the workflow early with a reason (clean termination, not failure). |
Error Handling
Each activity can configure retry logic and fallback behavior via the on_error field:
| Field | Type | Description |
|---|---|---|
on_error.retry | number | Times to retry the activity before giving up (default: 0) |
on_error.fallback | string | What to do after retries exhausted: skip, abort, notify_owner. Defaults to notify_owner. |
Circuit Breaker
A built-in circuit breaker (CIRCUIT_BREAKER_THRESHOLD = 3) monitors consecutive failures. If 3 or more consecutive activities fail with the same error pattern, the workflow aborts automatically. The failure reason is stored per activity result, enabling post-mortem analysis. This prevents infinite retry loops on systematic errors (e.g., a misconfigured API key causing every activity to fail the same way).
Same-tool Loop Detection
If the LLM calls the same tool 3+ times in a row, a steering hint is injected to break the loop. This prevents runaway tool-call patterns that waste budget without making progress.
Cancellation
Workflows support graceful cancellation via a cancellation token. When cancelled:
- The currently in-progress activity is allowed to finish (no mid-activity abort)
- No subsequent activities are started
- The workflow run is marked as cancelled, not failed
Cancel a running workflow via POST /agents/{id}/workflows/{binding}/cancel.
Budget Math
When using inline activities with a budget.total_per_run, the sum of all activity token_budget.max values must not exceed total_per_run. This is validated at parse time -- a mismatch prevents the agent from loading.
Activity 1: gather -> 4,096 tokens
Activity 2: deliver -> 1,024 tokens
-----
Sum: 5,120 tokens
budget.total_per_run: 6,000 tokens (>= sum, headroom for retries)Both per-activity and global budgets are enforced independently at runtime.
total_per_run to accommodate retries. In the example above, 880 tokens of headroom allow a retry of the smaller activity without exceeding the global budget.Complete Example: Email Triage Pipeline
This example demonstrates a complete multi-workflow pipeline. A watcher monitors Gmail in real-time. For each email, a triage activity classifies it and emits events. Separate workflows handle customer service and sales inquiries independently.
{
"workflows": {
"email-watcher": {
"trigger": {
"type": "watch",
"plugin": "gws",
"event": "email.new",
"command": "gmail +watch --format ndjson"
},
"description": "React to new emails in real-time",
"activities": [
{
"id": "triage",
"intent": "Classify the incoming email and route it",
"steps": [
"Read the email content from _watch_payload",
"Classify as: customer-service, sales-inquiry, or ignore",
"If not ignore, emit the appropriate event with email data"
],
"token_budget": { "max": 2048 }
}
],
"budget": { "total_per_run": 2048 }
},
"handle-cs": {
"trigger": { "type": "event", "sources": ["email.customer-service"] },
"description": "Handle customer service emails",
"activities": [
{
"id": "respond",
"intent": "Draft a helpful response to the customer service email",
"skills": ["@acme/skills/cs-templates"],
"token_budget": { "max": 3000 }
}
],
"budget": { "total_per_run": 3000 }
},
"handle-sales": {
"trigger": { "type": "event", "sources": ["email.sales-inquiry"] },
"description": "Handle inbound sales inquiries",
"activities": [
{
"id": "qualify",
"intent": "Qualify the lead and add to CRM",
"skills": ["@acme/skills/lead-qualification"],
"token_budget": { "max": 3000 }
}
],
"budget": { "total_per_run": 3000 }
}
}
}Read top to bottom: the watcher monitors Gmail. For each email, the triage activity classifies and emits an event. Each emission triggers the appropriate handler with the email data as inputs.