Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Weft The book

Writing a trigger

A trigger starts runs from outside. It has two bodies instead of one.

#[async_trait]
impl Node for CronNode {
    async fn setup_trigger(&self, ctx: ExecutionContext) -> WeftResult<()> {
        let spec: String = ctx.inputs.get("schedule")?;
        ctx.register_signal(Timer::cron(spec)).await
    }

    async fn run(&self, ctx: ExecutionContext) -> WeftResult<()> {
        ctx.pulse_downstream(ctx.fan_declared(&ctx.wake.record()?)).await
    }
}

setup_trigger runs once, when somebody activates the project. run fires every time the thing actually happens.

In the metadata:

"features": { "isTrigger": true },
"firesWith": { "chatId": "String", "text": "String", "caller?": "JsonDict" }

Setup

Read your inputs, decide what should wake you, and register it. That is all.

You never listen for anything yourself. No socket, no polling loop, no webhook handler. You say what should wake you and the runtime does the holding, the renewing and the checking that the event is real.

Two things to know about it.

Setup reaches out. A trigger that waits for Slack messages registers a subscription with Slack right there. If that cannot be done, activation fails on the spot naming what is missing, rather than leaving a trigger that looks armed and is dead.

Your inputs are frozen at that moment. Whatever they were is snapshotted alongside the registration and replayed onto ctx.inputs on every firing. The nodes upstream do not run again per event. When somebody changes one, they have to run weft resync, and that is why.

Register once per node per setup. A second call is a loud error rather than a trigger that quietly never fires.

Firing

run gets the event on ctx.wake, a bag with the same methods as ctx.inputs. Your declared inputs are there too, replaying the snapshot.

let chat_id: String = ctx.wake.get("chatId")?;

ctx.fan_declared(&ctx.wake.record()?) is the common shape: take the whole payload and fan its keys onto your same-named output ports, skipping anything you did not declare.

firesWith

Say exactly what the event carries. The ? goes on the name:

"firesWith": { "chatId": "String", "caller?": "JsonDict" }

It is checked both ways before your body runs. A payload missing a field you declared without ? is refused, and so is one carrying a field you never declared, at the top level and nested. weft run --fire checks a payload you typed by hand against it too.

One honest caveat: nothing enforces at build time that a trigger has a firesWith, or that a non-trigger does not. A repo test holds the shipped catalog to both. A node you write is held to neither, and a typo in one of the type strings is not caught when the metadata loads, so it reaches a real firing before it fails.

What can wake you

KindWhat it is
TimerA delay, a date, or a cron schedule. A delay counts from when it was asked for: the activation for a trigger, the await_signal call for a node that waits
FormA person filling something in
RouteAn inbound HTTP request
SocketAn inbound WebSocket
SocketListenA socket weft dials out to and holds
StreamListenA TCP or TLS stream
SseSubscribeA server-sent-event stream weft subscribes to
PollEndpointAn address weft checks on a timer
ProviderEventsA service’s own events, by topic, through its recipe

ProviderEvents is the one to reach for when the service has a recipe, because it takes care of both roads: a provider that pushes to you, and one you have to dial. Go and read events from a service.

Two things a trigger cannot do

It cannot be wired from another trigger, and nothing downstream of a trigger can be an infra node. Both are refused at compile time, because a trigger’s inputs are frozen at setup and provisioning happens before any event exists.

It also cannot sit inside a loop. It registers once for the project, so a per-iteration one is not a thing.

Testing one

rig.run_setup_trigger(&CronNode, json!({ "schedule": "0 9 * * *" })).await.ok()?;
assert_eq!(rig.registered_signals().len(), 1);

let outcome = rig.wake(json!({ "firedAt": 1700000000 }))
    .run(&CronNode, json!({})).await.ok()?;

fake is the top tier for a trigger. The live rig drives a plain run body, so a live test on a trigger is refused.