Skip to main content
Automations are the parts of an app that run on their own — on a schedule, or when an outside service sends in an event. You declare them on a process component in kazzle.config.ts. One component can carry as many triggers as you need.

The shape

{
  name: 'events',
  type: 'process',
  path: './components/events/index.ts',
  processMode: 'persistent', // or 'triggered'
  triggers: [
    { name: 'daily-digest', kind: 'schedule', schedule: '0 9 * * *', path: '/cron/daily-digest' },
    { name: 'stripe',       kind: 'webhook',                          path: '/webhook/stripe' },
  ],
}
Two things are happening here:
  • processMode picks the lifecycle — long-running server, or one-off run per trigger.
  • triggers[] lists the events that should fire this component.
The two pieces are independent. A persistent server can have a cron. An ephemeral process can have a webhook. Pick the lifecycle that fits the workload, then attach as many triggers as you like.

processMode

ModeWhat runsWhen to use
persistent (default)A long-running HTTP server. Triggers are POSTed into it.The component already serves HTTP, or holds state in memory (queues, websockets, caches).
triggeredThe entry script is spawned per trigger and exits.Pure background jobs — nightly cleanup, single Stripe webhook handler, etc. No idle servers.

Triggers

Each trigger has a name (unique within the component), a kind, and — depending on mode — a schedule and/or path.
FieldWhen requiredNotes
namealwaysUsed as the webhook URL segment and in logs. Kebab-case.
kindalways'schedule' or 'webhook'.
schedulewhen kind: 'schedule'5-field cron expression. Minute resolution is the floor.
pathwhen processMode: 'persistent'HTTP route on your server where the trigger lands.

Persistent mode — HTTP into the server

When a trigger fires for a persistent component, Kazzle POSTs to your server at the declared path. The request carries:
HeaderWhat it tells you
Authorization: Bearer ${KAZZLE_TRIGGER_SECRET}Validate this. Reject calls that don’t match.
x-kazzle-trigger-nameThe trigger’s name from the manifest.
x-kazzle-trigger-run-idOpaque ID for log correlation.
x-kazzle-triggered-bycron | webhook | manual.
For webhook triggers, the original request body is forwarded as the POST body. For schedule triggers the body is empty.
// components/events/index.ts (persistent mode)
const TRIGGER_SECRET = process.env.KAZZLE_TRIGGER_SECRET ?? '';

Bun.serve({
  port: Number(process.env.PORT),
  hostname: process.env.HOST,
  async fetch(req) {
    const url = new URL(req.url);

    if (req.method === 'POST' && url.pathname === '/cron/daily-digest') {
      if (req.headers.get('authorization') !== `Bearer ${TRIGGER_SECRET}`) {
        return new Response('Unauthorized', { status: 401 });
      }
      await sendDigest();
      return Response.json({ ok: true });
    }

    if (req.method === 'POST' && url.pathname === '/webhook/stripe') {
      if (req.headers.get('authorization') !== `Bearer ${TRIGGER_SECRET}`) {
        return new Response('Unauthorized', { status: 401 });
      }
      const event = await req.json();
      await handleStripe(event);
      return Response.json({ ok: true });
    }

    return new Response('not found', { status: 404 });
  },
});

Triggered mode — one-off per trigger

When a trigger fires for a triggered component, Kazzle spawns the entry script fresh and waits for it to exit. There is no path; the script learns which trigger fired from env vars.
Env varValue
TRIGGER_NAMEThe trigger’s name from the manifest.
TRIGGERED_BYcron | webhook | manual.
RUN_IDOpaque ID for log correlation.
WEBHOOK_PAYLOADJSON body (webhook triggers only).
// components/events/index.ts (triggered mode)
const trigger = process.env.TRIGGER_NAME;
const runId = process.env.RUN_ID;

if (trigger === 'daily-digest') {
  await sendDigest();
} else if (trigger === 'stripe') {
  const event = JSON.parse(process.env.WEBHOOK_PAYLOAD ?? '{}');
  await handleStripe(event);
}

console.log(`run ${runId} done`);
Triggered components have no idle machines on production — they spin up per call and shut down on exit.

Webhook URLs

POST https://api.kazzle.app/webhooks/{spaceId}/{appId}/{componentName}/{triggerName}
The triggerName segment must match a kind: 'webhook' entry in that component’s triggers[]. Unknown trigger names return 404.

Schedule resolution

Cron expressions are 5-field (minute, hour, day-of-month, month, day-of-week) and minute resolution is the floor. Sub-minute schedules are rejected at manifest validation time.

How runs are recorded

Each trigger fire writes a process_runs row with the trigger_name, triggered_by, run_id, and the run’s exit status. You can query these from your own code or inspect them in the app’s runs view.

Running out of credits

A failing run is recorded and logged, but the schedule keeps running on its normal cadence — a flaky run never disables the trigger. The one thing that stops a run is credits: every trigger fire is checked against the space’s balance, and while the space is out of credits (or has no billing set up) runs are skipped with a 402. This is self-recovering — the schedule stays armed and the next fire after you top up runs normally, with no manual resume.

Adding automations later

A simple app can start with no triggers and gain them later — add a daily summary, connect Stripe, run cleanup. The component’s lifecycle (processMode) and triggers (triggers[]) are independent, so you can change them without rewriting the rest of the app.