Skip to content

Triggers

A trigger node is how a canvas graph declares the events that start it. It’s the inbound side of a graph: emit nodes fire events out, trigger nodes bring events in. On publish, a graph’s trigger nodes register the graph as an event handler, so the same wiring you’d set up with spool handler set happens from the canvas.

A trigger node has a transport that says how the world reaches it:

  • bus (the default) subscribes to the project event bus. The graph runs whenever a matching event fires - a finished run, a spool emit, a delivery another hook republished.
  • webhook gives the graph its own inbound HTTP endpoint. Publishing the graph mints the URL and a signing secret; a POST to that URL starts the graph. Use it to catch a forge push, or any external system, directly.

A trigger with no transport is a bus trigger, so graphs authored before webhook triggers existed keep working unchanged.

A bus trigger names the event pattern that starts the run:

{ "id": "on_failure", "type": "trigger",
"config": { "event": "run.failed" } }

The event value is an event-type pattern, the same kind a handler’s on list takes:

  • an exact type - run.failed, deploy-done
  • a prefix wildcard - run.* matches every run.<...>: run.started, run.awaiting, run.wait_expired, run.succeeded, run.failed and run.cancelled. Name the three terminal ones when a run parking on an approval should not count as one finishing
  • * - every event

A webhook trigger owns an endpoint the graph provisions on publish. Reach for one when this graph is the whole reaction to one sender and its endpoint should live and die with it: publishing mints the URL and secret, deleting the graph reaps them.

That is what the shape is for - webhook input that is not a repo push. A form submission, a device checking in, a vendor callback, a payment provider, an alert from something that only speaks webhooks. One sender, one graph, one endpoint that disappears with it.

For a repo, use spool repo add instead, and not as a matter of taste:

Terminal window
spool repo add https://github.com/me/app --credential gh-read

That keeps one endpoint per forge however many graphs or pipelines react to it, puts the routing in one predicate you can read rather than spread across documents, and builds the pipeline the pushed commit shipped with. A trigger node per graph means a forge registration per graph, and ten graphs reacting to one repo means ten webhooks to create and later remember to delete (CI from a repo).

{ "id": "on_push", "type": "trigger",
"config": { "transport": "webhook", "manifest": "github", "event": "push" } }
  • manifest (optional) selects signed-delivery verification and payload shaping for a known sender. github verifies the X-Hub-Signature-256 HMAC and shapes the push into fields like sha, ref and repo. Leave it off for a plain token-secret webhook.
  • event (optional) filters which delivery subtype starts the run (push, pull_request). Empty runs on any delivery.

The endpoint is an ordinary event hook underneath, named graph.<graph>.<node>, and its deliveries reach the bus like any other hook’s. What differs is who owns it: that section has the three things that decide which to reach for.

One consequence of the endpoint being keyed to the node: renaming the trigger node re-mints it. Publishing after a rename drops graph.<graph>.<old-id> and creates graph.<graph>.<new-id> with a new URL and a new secret, so whatever was posting to the old one is now posting nowhere. Re-register the sender, or leave a live trigger node’s id alone.

Publishing the graph mints the endpoint; the trigger inspector shows the ingest URL, a one-time secret, and a Register on forge control that creates the webhook on the repo for you (GitHub, Gitea, or Forgejo) given a repo and a forge admin credential - no manual paste. Or add it to your repo’s webhook settings by hand with the shown URL + secret. The secret is shown once - a re-publish keeps the same one. A token trigger’s secret can be replaced from the Connections page (rotate secret) or with spool event-hook rotate <name>; a manifest trigger signs with a key instead, which is replaced by deleting the hook and publishing again. CI from a repo walks through the whole push-to-build path.

A published webhook trigger's inspector: the ingest URL a forge posts to, the signing secret shown once at publish, and the register on forge button that creates the hook for you.

The node’s output is the event, or shaped delivery, that started the run. Downstream nodes read it through nodes.<trigger-id>.output, and because a graph receives the triggering event as its run input, they can read the same envelope through input. A run.failed bus trigger reads input.payload.error, input.payload.run_id, and the rest of the lifecycle payload; a github webhook trigger reads input.sha, input.ref, and the other shaped fields (repo, sender).

A webhook trigger with no manifest is not shaped, so it hands you the delivery as it arrived:

{ "body": { "sku": "TENT-2P", "qty": 12 }, "query": { "source": "shop" } }

Read a posted field as input.payload.body.sku and a query-string one as input.payload.query.source - not input.sku, which is the shaped github case above and is the mistake this shape invites. Query values are always strings; body values keep their JSON types. A POST with no body at all arrives as an empty object, so input.payload.body.sku finds nothing rather than failing; a body that is not JSON (form-encoded, plain text) arrives verbatim as a string, so guard when you expect one.

The sender authenticates with the secret in the X-Spool-Hook-Secret header - see events and handlers for a worked curl.

When you publish a graph, its trigger nodes reconcile into an event-handler registration named after the graph. The handler’s on list is the union of every trigger node’s event. Publish a graph with two bus trigger nodes (run.failed and deploy-done) and it registers as a graph handler matching both.

This is the canvas-native equivalent of:

Terminal window
spool handler set <graph> --graph <graph> --on run.failed,deploy-done

A webhook trigger also provisions its owned endpoint at this point and subscribes the graph to the event that endpoint emits, so there’s no separate hook or handler to set up - the trigger node is the whole ingress.

Remove all the trigger nodes and republish, and the registration (and any owned webhook) is dropped - the graph goes back to being a workflow you run by hand or spawn, with no event subscription.

Only the registration publishing created is dropped. A handler you set yourself to run this graph, off a shared hook, survives publishing it: that is the ordinary shape for repo CI, and reaping it would unwire the repo every time somebody opened the graph and pressed publish.

A notify graph wires one trigger to one connector node. The trigger starts the graph on any failed run; the connector posts the error to Matrix:

{
"version": "hyperspool-graph/v1",
"nodes": [
{ "id": "on_failure", "type": "trigger",
"config": { "event": "run.failed" } },
{ "id": "post", "type": "matrix.send_message",
"config": {
"with": { "homeserver": "https://matrix.example",
"room_id": "!abc:example" },
"input": { "text": { "$template": "run failed: {{ input.payload.error }}" } },
"credential": "matrix"
} }
],
"edges": [ { "from": "on_failure", "to": "post" } ]
}

Publish it, and every run.failed on the project bus posts to the room. No separate subscription to set up - the trigger node is the subscription.

  • Connectors - the outbound API call the example uses.
  • Events and handlers - what handlers are, what a graph handler receives, and how emit nodes fire events out.
  • Event catalog - discovering which events you can put in a trigger node.