/docs / workflows / triggers

Workflow Triggers

Triggers define when a workflow runs. The Agent owns the schedule; the workflow owns the procedure. The same workflow could run at 7am in one Agent and 9am in another -- the trigger is part of the binding, not the workflow itself.

Trigger types

Every workflow binding must include a trigger object with a type field. Five trigger types are available:

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

Schedule

Schedule triggers fire on a standard 5-field cron expression. The expression is evaluated every 60 seconds against the agent's configured timezone.

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

The example above fires at 7:00 AM every weekday (Monday through Friday). Any valid 5-field cron expression is accepted:

FieldValuesExample
Minute0-590
Hour0-237
Day of month1-31*
Month1-12*
Day of week0-6 (Sun-Sat)1-5

A full workflow binding with a schedule 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"
    }
  }
}

Heartbeat

Heartbeat triggers fire at a recurring interval. They can optionally be constrained to a time window -- outside the window, heartbeats are silently skipped.

{ "type": "heartbeat", "interval": "30m", "window": "08:00-18:00" }

Heartbeat fields

FieldTypeRequiredDescription
intervalstringyesDuration string: "30m", "1h", "2h30m"
windowstringnoActive hours ("HH:MM-HH:MM"). Outside the window, heartbeats are skipped.

The interval is measured from the end of the previous run, not the start. This prevents overlapping runs when activities take longer than the interval.

Event

Event triggers fire when a matching event occurs in the EventBus. Events can originate from other workflows (via the emit tool), watch triggers, platform capabilities, or system lifecycle events.

{ "type": "event", "sources": ["email.urgent", "calendar.*"] }

Event fields

FieldTypeDescription
sourcesstring[]Event source patterns to match. Supports exact (email.urgent) and wildcard (email.*).

Source matching

PatternMatches
email.urgentExact match only
email.*Any event starting with email.
An empty sources array is valid JSON but the trigger will never fire -- always include at least one source.

The event payload becomes the triggered workflow's inputs, available as _event_payload. For more details on the event system, event sources, and event envelopes, see Event System.

Event trigger example

A workflow that triggers on customer service emails emitted by an upstream triage workflow:

{
  "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 }
  }
}

Watch

Watch triggers spawn a long-running plugin process that outputs NDJSON (one JSON object per line) to stdout. Each line triggers the bound activities with the parsed JSON as input.

{
  "type": "watch",
  "plugin": "gws",
  "event": "email.new",
  "command": "gmail +watch --format ndjson",
  "restart_delay_secs": 5
}

Watch fields

FieldTypeRequiredDefaultDescription
pluginstringyes--Plugin slug
eventstringno--Plugin event name. Enables auto-emission into EventBus.
commandstringno""CLI args appended to plugin binary. Required if event is not set.
restart_delay_secsu64no5Seconds to wait before restarting on crash

How it works

  1. Spawns <plugin-binary> <command> as a long-running subprocess
  2. Plugin outputs NDJSON (one JSON object per line) to stdout
  3. Each line triggers the bound activities with the parsed JSON as _watch_payload input
  4. If event is set, each line also auto-emits into the EventBus as {plugin}.{event}
  5. 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.

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 can then subscribe to these events:

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

Manual

Manual triggers only fire via an API call or explicit user request. They are useful for workflows that should not run automatically but need to be available on demand.

{ "type": "manual" }

Trigger a manual workflow via:

POST /agents/{id}/workflows/{binding}/run

Template substitution in commands

Watch trigger commands support {{key}} placeholders, which are 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).

Input values & template substitution in triggers

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.

{
  "trigger": { "type": "schedule", "cron": "{{sync_time}}" }
}

If the agent's input_values contains { "sync_time": "0 9 * * 1-5" }, the cron expression resolves to 0 9 * * 1-5 at trigger evaluation time. This lets end users customize workflow timing and parameters without editing the agent package.

Full example: email triage pipeline

This example demonstrates how triggers chain together. A watch trigger monitors Gmail, a triage activity classifies each email and emits events, and event triggers handle each category 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, triage classifies and emits. Each emission triggers the appropriate handler with the email data as inputs.

Validation rules

Triggers are validated when the agent loads. Invalid triggers cause the individual binding to be skipped (not the entire agent):

  • Trigger type must be one of: schedule, heartbeat, event, watch, manual
  • Schedule triggers must have a valid 5-field cron expression
  • Heartbeat triggers must have a valid interval (e.g., "30m", "1h")
  • Event triggers should have at least one entry in sources
  • Watch triggers must have a plugin and either event or command
  • All {{key}} placeholders in commands must match an input key exactly
Lenient parsing: Individual workflow bindings that fail to parse (e.g., invalid trigger format) are skipped with a warning -- they do not prevent the agent from loading. The agent still appears in the UI with its remaining valid workflows.

Related

  • Workflows Overview -- workflow bindings, budget math, and the full binding schema
  • Activities -- what happens after a trigger fires: sequential activity execution, context chaining, and error handling
  • Event System -- event sources, event envelopes, source matching, and system events