/docs / agents / workflows

Workflows & Automation

Workflows are the automation engine. Each workflow binding pairs a trigger (when to run) with activities (what to do). The Agent owns the schedule; the workflow owns the procedure.

Workflows are defined in the workflows map inside agent.json. An agent without workflows is valid — it provides only a persona and skills for interactive chat.
Key principle: The workflow doesn't decide when it runs. The Agent does. The same procedure could run at 7am in one Agent and 9am in another.

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

FieldTypeRequiredDescription
refstringnoExternal workflow qualified name (@org/workflows/name@version). Optional when using inline activities.
triggerobjectyesWhen this workflow runs (see Trigger Types)
descriptionstringnoHuman-readable description
inputsmapnoDefault inputs passed to the workflow on trigger
activitiesarraynoInline activity definitions. When present, the workflow runs inline — no external ref needed.
budgetobjectnoToken budget constraints (total_per_run)
emitstringnoEvent name to emit on completion. By convention, prefix with the agent slug 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

FieldTypeRequiredDescription
idstringyesUnique activity ID within the binding
intentstringyesTask description — what the LLM should accomplish
stepsstring[]noStep-by-step hints for the LLM
skillsstring[]noSkill references available to this activity (@org/skills/name)
mcpsstring[]noMCP server slugs available to this activity
modelstringnoModel override ("sonnet", "haiku", "opus")
token_budgetobjectnoPer-activity token limit: { "max": 4096 }
on_errorobjectnoError handling: { "retry": 1, "fallback": "skip" }
min_iterationsnumbernoForce LLM to continue even if it wants to stop early

How Activities Execute

  • 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, 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:

FieldTypeDescription
started_attimestampWhen the activity began executing
completed_attimestampWhen the activity finished
input_tokensnumberToken usage for input (prompt)
output_tokensnumberToken usage for output (completion)
error_patternstringError pattern tracked for circuit breaker (if failed)
failure_reasonstringHuman-readable failure reason (if failed)

Built-in Tools

Two tools are always available inside activities:

ToolPurpose
emitEmit an event into the EventBus. Other agents can subscribe to it.
exitStop the workflow early with a reason (clean termination, not failure).

Error Handling

FieldTypeDescription
on_error.retrynumberTimes to retry the activity before giving up (default: 0)
on_error.fallbackstringWhat to do after retries exhausted: skip, abort, notify_owner. Defaults to notify_owner.

Circuit breaker (CIRCUIT_BREAKER_THRESHOLD = 3): If 3 or more consecutive activities fail with the same error pattern, the workflow aborts automatically. This prevents infinite retry loops on systematic errors.

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.

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.

Trigger Types

TypeFieldsDescription
schedulecronFires on a cron schedule (standard 5-field expression)
heartbeatinterval, windowFires at a recurring interval, optionally limited to a time window
eventsourcesFires when a matching event occurs
watchplugin, event, commandLong-running plugin process that emits NDJSON events
manualOnly fires by explicit user request or API call

Schedule

{ "type": "schedule", "cron": "0 7 * * 1-5" }

Standard 5-field cron. Evaluated every 60 seconds against the agent's configured timezone.

Heartbeat

{ "type": "heartbeat", "interval": "30m", "window": "08:00-18:00" }
FieldTypeRequiredDescription
intervalstringyesDuration string: "30m", "1h", "2h30m"
windowstringnoActive hours ("HH:MM-HH:MM"). Outside the window, heartbeats are skipped.

Event

{ "type": "event", "sources": ["email.urgent", "calendar.*"] }
FieldTypeDescription
sourcesstring[]Event source patterns to match. Supports exact (email.urgent) and wildcard (email.*).

An empty sources array is valid JSON but the trigger will never fire — always include at least one source.

Watch

{
  "type": "watch",
  "plugin": "gws",
  "event": "email.new",
  "command": "gmail +watch --format ndjson",
  "restart_delay_secs": 5
}
FieldTypeRequiredDescription
pluginstringyesPlugin slug
eventstringnoPlugin event name. Enables auto-emission into EventBus.
commandstringnoCLI args appended to plugin binary. Required if event is not set.
restart_delay_secsnumbernoSeconds to wait before restarting on crash (default: 5)

How it works:

  • Spawns <plugin-binary> <command> as a long-running subprocess
  • Plugin outputs NDJSON (one JSON object per line) to stdout
  • Each line triggers the bound activities with the parsed JSON as _watch_payload input
  • If event is set, each line also auto-emits into the EventBus as {plugin}.{event}
  • On crash, restarts after restart_delay_secs with exponential backoff (max 300s)
Always provide a command fallback alongside event. If the plugin manifest doesn't declare the event, resolution fails silently. command ensures the watcher starts regardless.

Template Substitution in Commands

Watch trigger commands support {{ key }} placeholders, substituted from the agent's input values at runtime:

"command": "gmail +watch --format ndjson --project {{gcp_project}}"

The placeholder name must exactly match an input key. Unmatched placeholders are left as literal text (which will cause failures).

All trigger configs — not just watch commands — support {{ key }} template substitution. Values are resolved from agents.input_values, which users set via the Settings UI or the API. This lets end users customize workflow timing and parameters without editing the agent package.

Manual

{ "type": "manual" }

Only fires via API call (POST /agents/{id}/workflows/{binding}/run) or explicit user request.

Event System

Event triggers let workflows react to things that happen — emails arriving, workflows completing, platform changes detected.

Event Sources

SourceMechanismExample Values
emitActivity calls emit toolemail.customer-service, lead.qualified
watchWatch trigger auto-emitsgws.email.new, gws.calendar.changed
platformPlatform capabilitiescalendar.changed, email.received
systemNebo lifecycleworkflow.{id}.completed, workflow.{id}.failed

Event Envelope

{
  "source": "email.customer-service",
  "payload": { "from": "j@example.com", "subject": "Order issue" },
  "origin": "workflow:email-triage:run-550e8400",
  "timestamp": 1709740800
}

The payload becomes the triggered workflow's inputs (available as _event_payload).

Source Matching

PatternMatches
email.urgentExact match only
email.*Any event starting with email.

Emit Namespace

Events emitted from activities use the source name as-is — there is no automatic namespace prefix. Activities must include the full source name to avoid collisions across agents.

emit(source: "executive-assistant.briefing.ready", payload: {...})

Convention: Prefix event sources with the agent slug to prevent cross-agent collisions. Other agents subscribe to the full source name.

Each emit call is a discrete event. If an activity emits 5 events, each fires independently against all active subscriptions. This enables fan-out pipelines.

System Events

EventWhen
workflow.{id}.completedA workflow run finishes successfully
workflow.{id}.failedA workflow run fails

System events enable workflow chaining: A completes, then B triggers.

Progress Events

EventWhenPayload
ActivityStartedEach activity begins executingActivity ID, workflow run ID
TaskUpdatedDuring activity executionProgress details, current activity state

These events are broadcast over WebSocket, enabling the UI to show real-time workflow progress.

Event-Only Watches

A watch with event set but no activities is valid. It relays plugin output into the EventBus without processing anything inline:

{
  "email-relay": {
    "trigger": {
      "type": "watch",
      "plugin": "gws",
      "event": "email.new",
      "command": "gmail +watch --format ndjson"
    },
    "description": "Relay new email events into the EventBus"
  }
}

Other agents subscribe:

{
  "handle-emails": {
    "trigger": { "type": "event", "sources": ["gws.email.new"] },
    "activities": [...]
  }
}

Example: Email Triage Pipeline

{
  "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: watcher monitors Gmail. For each email, triage classifies and emits. Each emission triggers the appropriate handler with the email data as inputs.

Validation Rules

  • Each binding must have a trigger (required)
  • Either ref or activities must be present (one defines what runs)
  • Trigger type must be one of: schedule, heartbeat, event, watch, folder, manual
  • Lenient parsing: Individual workflow bindings that fail to parse are skipped with a warning — they do not prevent the agent from loading
  • Schedule triggers must have a valid 5-field cron expression
  • Heartbeat triggers must have a valid interval
  • Event triggers should have at least one entry in sources
  • Watch triggers must have a plugin and either event or command
  • Activity IDs must be unique within each binding
  • If budget.total_per_run > 0, the sum of all activity token_budget.max must not exceed it
  • All {{ key }} placeholders in commands must match an input key exactly
  • An Agent with no workflows is valid (chat-only + persona + skills)