# @siftline/actions (/docs/packages/actions)



```bash
npm install @siftline/actions
```

ESM only. Node 22.14 or newer. Depends on `@siftline/core` for the Decision and Recipe types.

`defineActions(definitions)` declares your Actions, keyed by Action id. Each value is
`{ kind, config }`, with the config typed by that kind's Adapter. Every config is parsed against
its kind's `configSchema` at once, and the ids stay literal keys in the result.
`ActionId<typeof actions>` reads those ids back as a union, for typing Rules:
`Rule<Q, ActionId<typeof actions>>` rejects an Action id you did not define.
`dispatch(decision, actions, recipe, { fetch?, signal? })` sends a routed Decision to the Action
it selected and returns `{ action, request, response }`, or `null` when the Decision went to
Review or selected no Action.

Underneath, building a request and sending it are two steps. An Adapter's
`build(decision, config, recipe)` turns a Decision into an `ActionRequest`: method, URL,
headers, body and the idempotency key. `perform(request, { fetch?, signal? })` sends it.
`dispatch` only composes the two. Preview is `build` without `perform`, and nothing in a request
depends on a clock or randomness, so the same Decision, config and Recipe always build the same
bytes. The guide shows [both requests](/docs/guide/send-an-action).

## Adapters [#adapters]

| Export                 | `kind`                   | Body                                                      |
| ---------------------- | ------------------------ | --------------------------------------------------------- |
| `webhook`              | `webhook`                | The Decision line, no envelope. `parseDecision` reads it. |
| `slackIncomingWebhook` | `slack_incoming_webhook` | One mrkdwn message with each answer and its confidence.   |

`adapters` maps each `kind` to its adapter, and every adapter carries a `configSchema` for a
config that arrives as JSON. The webhook config is `{ url, secret?, headers? }`; with a
`secret` the request carries `X-Siftline-Signature: sha256=<HMAC-SHA256 of the body>`. The
Slack config is `{ url }`.

Every request carries `Idempotency-Key: <decision.id>:<decision.action>` and a `User-Agent`
naming the package version. Slack ignores the key, so a redelivery past your own guard posts
twice.

## Sending [#sending]

`perform` sends once, with no retries and no timeout of its own, and keeps the first 4096 bytes
of the response body, cut on a code-point boundary. It returns `{ status, body, truncated }`.
`dispatch` sends through `perform` and adds no retry or timeout either. `fetch` defaults to the
global one, read on each call. The option is typed narrower than the global `fetch`, so the
global one and a stub both fit without a cast. `signal` goes to `fetch` unchanged.

## Errors [#errors]

| Class               | `code`          | Thrown when                                                                                                                                       |
| ------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ActionBuildError`  | `action_build`  | An unknown kind or an invalid config in `defineActions`, an Action id `dispatch` was not given, or `build` on a Decision that selected no Action. |
| `ActionFailedError` | `action_failed` | A non-2xx status or a `fetch` that threw. Carries `status` and `retryable`, true for 408, 429, 5xx and network failures.                          |

Both extend core's `SiftlineError`.
