# Automations

> Triggers your app fires and actions your app does, inside the rules a merchant builds.

Source: https://storeware.ai/docs/extensions/automations

A merchant's automations are rules the shop runs by itself: when something happens, check a few things, then do some steps. Your app can add to both ends. A **trigger** is something your app says happened, such as a customer reaching a loyalty tier. An **action** is something your app does when a rule reaches that step, such as adding points.

Both are declared in your manifest. They appear in a shop once the merchant has installed the release that declares them, under your app's name, in the admin and in what the merchant assistant reads.

```toml
[[automations.triggers]]
key = "tier_reached"
label = "A customer reaches a tier"
description = "Fired when a customer's points take them into a new tier."

[[automations.triggers.fields]]
key = "tier"
label = "The tier"
type = "text"

[[automations.triggers.fields]]
key = "points"
label = "Their points"
type = "number"

[[automations.actions]]
key = "add_points"
label = "Add points"
url = "https://loyal.example.com/storeware/actions/add-points"

[[automations.actions.fields]]
key = "points"
label = "Points"
type = "number"
required = true

[[automations.actions.fields]]
key = "reason"
label = "Reason"
type = "text"
```

A trigger or action `key` is lower case letters and underscores, up to 32 characters, with no digits. A field `key` is lower case letters, digits and underscores. You may declare up to 20 triggers and 20 actions, each with up to 20 fields.

| Trigger field type | What you send                                       | A condition can test               |
| ------------------ | --------------------------------------------------- | ---------------------------------- |
| `text`             | A string, up to 2000 characters                     | is, is not, one of, contains       |
| `number`           | A number                                            | is, more than, at least, less than |
| `boolean`          | `true` or `false`                                   | is, is not                         |
| `date`             | An ISO 8601 date, such as `2026-11-07`              | is, is not, known, not known       |
| `url`              | An `http` or `https` address, up to 2000 characters | is, is not, one of, contains       |

Action fields are `text` (one line), `multi_line_text`, `number`, `boolean`, `url` (https) and `select`, which needs `options`.

## Firing a trigger

Call `POST /api/v1/app/events` with your app's access token, naming the trigger by its `key` and sending its fields as `data`:

```ts
await fetch('https://api.storeware.ai/api/v1/app/events', {
  method: 'POST',
  headers: { authorization: `Bearer ${accessToken}`, 'content-type': 'application/json' },
  body: JSON.stringify({ trigger: 'tier_reached', data: { tier: 'gold', points: 1200 } }),
});
```

It answers `202` with the trigger as a rule names it, `app:<your slug>:tier_reached`, and the data that was kept. The shop's rules on that trigger run shortly after.

- The trigger must be declared in the release the shop has installed. Anything else is refused with `400 app_trigger_not_declared`.
- A field you did not declare is dropped. A declared field of the wrong type is refused with `400 app_trigger_data_invalid`, naming the field. A field you leave out, or send as `null`, is read as unknown.
- `data` may be at most 16 KB.
- Each installation may fire 60 triggers a minute and 1000 an hour. Past that the answer is `429` with `Retry-After`.

A trigger is not sent to any app's webhooks, your own included. It exists for the shop's automations alone.

A merchant writes a condition on your trigger's fields as `data.<key>`, so `data.tier is gold` or `data.points is at least 1000`.

## Doing an action

When a rule reaches your action, Storeware sends a signed `POST` to the action's `url`:

```json
{
  "storeId": "…",
  "automationId": "…",
  "runId": "…",
  "step": { "id": "bonus", "index": 1 },
  "action": "add_points",
  "fields": { "points": 50, "reason": "Order 1042" },
  "event": {
    "id": "…",
    "topic": "order.paid.v1",
    "fields": { "order.id": "…", "order.number": "1042" }
  }
}
```

`fields` are what the merchant filled in. Text may name a field of the event in braces, such as `Order {order.number}` or `Welcome to {data.tier}`, and arrives filled in. `event` is the event that started the rule, or `null` for a rule run on a schedule or by hand. Its `fields` use the names conditions use. For a platform event they are sent only when your app's scopes cover that event, so an app without order access is told the order's id and topic and nothing else. For an app's trigger they are the data that app sent.

Verify the request with `verifyActionRequest`. It is signed with your installation's secret as the kind `action`, so a webhook or a block request replayed at this address fails.

```ts
import { verifyActionRequest, IDEMPOTENCY_HEADER } from '@storeware/plugin-sdk/actions';

export async function POST(request: Request) {
  const rawBody = await request.text();
  const action = await verifyActionRequest({
    secret: installationSecret,
    rawBody,
    signatureHeader: request.headers.get('storeware-signature'),
  });
  const key = request.headers.get(IDEMPOTENCY_HEADER); // `<runId>:<step index>`

  if (!(await alreadyDone(key))) await addPoints(action.storeId, action.fields);
  return new Response(null, { status: 204 });
}
```

Answer with any `2xx` once it is done. Any other status, no answer within 10 seconds, or an answer over 16 KB fails the step, and the merchant reads why on the rule's runs. A rule that fails ten times in a row switches itself off. The same run and step can reach you twice after a retry, with the same `idempotency-key` both times, so do the work once per key.

Backtests never call your app. When a merchant tries a rule against the last 30 days, your action is shown as what would have happened.

## When your app is removed

If a merchant uninstalls your app, every rule that starts from one of your triggers or uses one of your actions is switched off, and the merchant is told an app it used was removed. Installing your app again and switching the rule back on is all it takes. A rule is also refused, when it is saved or switched on, if it names a trigger, action or field the installed release does not declare, so removing one from a new release stops rules from being switched on with it.
