> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kazzle.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Automations

> Schedule jobs and respond to webhooks from inside a Kazzle app.

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

```ts theme={"theme":"material-theme-darker"}
{
  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`

| Mode                   | What runs                                                | When 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).    |
| `triggered`            | The 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`.

| Field      | When required                    | Notes                                                    |
| ---------- | -------------------------------- | -------------------------------------------------------- |
| `name`     | always                           | Used as the webhook URL segment and in logs. Kebab-case. |
| `kind`     | always                           | `'schedule'` or `'webhook'`.                             |
| `schedule` | when `kind: 'schedule'`          | 5-field cron expression. Minute resolution is the floor. |
| `path`     | when `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:

| Header                                           | What it tells you                             |
| ------------------------------------------------ | --------------------------------------------- |
| `Authorization: Bearer ${KAZZLE_TRIGGER_SECRET}` | Validate this. Reject calls that don't match. |
| `x-kazzle-trigger-name`                          | The trigger's `name` from the manifest.       |
| `x-kazzle-trigger-run-id`                        | Opaque ID for log correlation.                |
| `x-kazzle-triggered-by`                          | `cron` \| `webhook` \| `manual`.              |

For webhook triggers, the original request body is forwarded as the POST body. For schedule triggers the body is empty.

```ts theme={"theme":"material-theme-darker"}
// 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 var           | Value                                   |
| ----------------- | --------------------------------------- |
| `TRIGGER_NAME`    | The trigger's `name` from the manifest. |
| `TRIGGERED_BY`    | `cron` \| `webhook` \| `manual`.        |
| `RUN_ID`          | Opaque ID for log correlation.          |
| `WEBHOOK_PAYLOAD` | JSON body (webhook triggers only).      |

```ts theme={"theme":"material-theme-darker"}
// 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

```text theme={"theme":"material-theme-darker"}
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.
