Triggers

A trigger is a way to start an agent without anyone making an HTTP request. Cron fires on a schedule; webhooks fire on an inbound POST; boot triggers fire once when Coulisse starts. All three convert to the same shape — a task enqueued via the queue — so the agent runtime doesn't know or care how it was summoned.

This is the primitive that makes Coulisse feel like an office instead of a request handler: agents wake up because something happened, not because someone is waiting.

Why this is platform-agnostic

There's no chat-platform-specific code in Coulisse. The webhook trigger (coming next) accepts JSON POSTs from anything that can speak HTTP. Connecting Slack means pointing Slack's built-in outgoing webhooks at Coulisse. Connecting GitHub means setting up a webhook on the repo. Anything else that can POST JSON can summon an agent the same way. Coulisse doesn't know the source — it sees an HTTP request.

The cron trigger is purely internal — zero external dependencies.

Cron triggers

Configure under the top-level triggers: list in coulisse.yaml:

triggers:
  - name: daily-standup
    type: cron
    schedule: "0 9 * * *"      # every day at 09:00
    agent: pm
    prompt: "Standup matin — résume l'activité d'hier en 5 puces."

  - name: hourly-watch
    type: cron
    schedule: "0 * * * *"       # every hour at :00
    agent: user-tester
    prompt: "Une phrase sur le ressenti du moment."

Fields:

  • name — stable identifier used in logs and admin views. Must be unique within the file.
  • type: cron — the discriminator. Other types (webhook) arrive later.
  • schedule — POSIX cron expression. Either 5-field (min hour day-of-month month day-of-week) or 6-field with leading seconds (sec min …). The 5-field form is normalised to 6-field with a leading 0 seconds. Schedules are validated at startup; bad expressions refuse to boot.
  • agent — name of the agent (or experiment) to invoke. Must exist in agents: / experiments:.
  • prompt — static user message passed to the agent on each fire. Templating from trigger payload arrives with the webhook trigger.

When the trigger fires, Coulisse enqueues a task and a worker runs the agent through the same handler the sync /v1/chat/completions endpoint uses. The agent gets its full preamble, MCP tools, subagent dispatch, and narration — nothing about background runs is different. Watch them in /admin/live.

User identity

Cron-triggered tasks run as default_user_id (from the top of coulisse.yaml). If unset, they run as a synthetic cron user. Memory partitions are honoured: if daily-standup calls pm with default_user_id: main, it sees the same memory bucket as a human who sends a chat request as main.

Watching cron fire

Tail the log; you'll see one line per arm and one per fire:

INFO cron trigger armed   trigger=daily-standup agent=pm
INFO cron trigger fired   trigger=daily-standup agent=pm task_id=…

Or open /admin/live — tasks created by triggers appear in the Tasks panel the same way dispatch_task tasks do, with the trigger's prompt as the initial message and the agent name as written in YAML.

Boot triggers

A type: boot trigger fires exactly once when Coulisse starts. Use it for "wake up and decide what to do" prompts that should run on every coulisse start — e.g. asking an orchestrator agent to read the queue's leftovers and decide whether a standup is warranted, without forcing a ritual on every restart.

triggers:
  - name: wakeup
    type: boot
    agent: pm
    prompt: |
      You just came back online. Check `tasks_status` for what was running
      before the stop, look at recent commits, and decide whether to post
      a standup. Silence is fine when nothing demands attention.

Fields:

  • type: boot — discriminator.
  • agent, prompt — same as cron: which agent runs, with what initial message.

The task is enqueued during coulisse start, after the worker pool is up. Combined with the boot-time reaper that marks orphaned running tasks as errored, this gives the wake-up agent everything it needs to assess state and resume work — see Async tasks for the queue semantics.

Webhook triggers

A type: webhook trigger declares an HTTP path; Coulisse exposes POST <path> and fires the trigger on each request. This is the universal connector for outside systems — anything that can POST JSON can summon an agent. No chat-platform code in Coulisse.

triggers:
  - name: chat-mention
    type: webhook
    path: /hooks/chat-mention        # must start with /hooks/
    agent: pm
    prompt: "Message de {{sender}} dans {{room_name}} : {{body}}"

Fields beyond the cron shape:

  • type: webhook — discriminator.
  • path — HTTP path Coulisse exposes. Must start with /hooks/ to stay clear of the proxy (/v1/*), studio (/admin/*), and OAuth callbacks (/mcp/*). Must be unique across all webhook triggers.
  • agent — name of the agent (or experiment) to invoke. Accepts the same {{a.b.c}} templating as prompt, so one webhook can route to different agents based on the inbound payload (see Templated agent below).
  • prompt — template. {{a.b.c}} placeholders pull values from the JSON payload by dot-path. Missing paths render as the literal {{ a.b.c }} so debugging is obvious. Static prompts (no placeholders) work too — they pass through unchanged.

Fire it with curl:

curl -X POST http://localhost:8421/hooks/chat-mention \
  -H 'Content-Type: application/json' \
  -d '{"sender":"alice","room_name":"engineering","body":"@coulisse what is the state of the build?"}'

Response:

{ "ok": true, "task_id": "cb9b91c4-54db-4b8c-a564-08282e643c25" }

The task appears in /admin/live like any other.

Templated agent

The agent field accepts the same {{a.b.c}} templating as prompt. This lets one webhook fan out to different agents based on whatever the inbound payload carries — useful when a bridge POSTs one event per mentioned agent:

triggers:
  - name: chat-mention
    type: webhook
    path: /hooks/chat-mention
    agent: "{{agent}}"
    prompt: "@{{sender}} in #{{room}}: {{body}}"

The bridge does the iteration on its side and calls the same webhook N times, once per mentioned agent:

curl -X POST http://localhost:8421/hooks/chat-mention \
  -d '{"agent":"pm","sender":"almaju","room":"standup","body":"any release blockers?"}'

curl -X POST http://localhost:8421/hooks/chat-mention \
  -d '{"agent":"coder","sender":"almaju","room":"standup","body":"any release blockers?"}'

Two tasks land on the queue, one per agent.

A templated agent field is not cross-validated at config load — the value isn't known until a request arrives. If the resolved name doesn't match any agent, the worker errors the task with an "unknown agent" message; you'll see it in /admin/live. If the placeholder fails to resolve at all (the path is missing from the payload), the webhook returns 400 Bad Request and nothing is enqueued.

What's not here yet

  • Per-trigger user_id. Today every trigger fires as the same default_user_id. A future field will let triggers run as different synthetic users, useful for partitioning memory between scheduled jobs.
  • Skip-on-overlap. If a cron fires while the previous run is still going, both queue up. A skip_if_running: true field would let users opt into "only one at a time."
  • Signature verification on webhooks. Anyone who can reach /hooks/<path> can fire the trigger. For Internet-facing deployments you'd want a shared secret or HMAC check, configurable per trigger. Today the assumption is loopback or trusted network.