# Automations

An automation is a sequence of steps that runs for one contact when something happens in your app: a welcome email when someone signs up, a thank-you after a payment, a reminder if a customer has not left a review three days later. Your app sends events; Ha Tang runs the steps. The API works like Resend Automations.

## Before you start

- Published [templates](https://hatang.dev/docs/guides/email/templates.md) for the emails the automation sends, with a `From` address on a verified domain.
- For the API, a key with `email:write`.

## 1. Define your events

An automation starts from an **event**. Define events under **Email → Automations → Events** or with the API. A schema is optional; when you give one, Ha Tang checks and converts the payload fields (`string`, `number`, `boolean`, `date`):

**cURL**

```bash
curl -X POST https://api.hatang.dev/events \
  -H "Authorization: Bearer $HATANG_API_KEY" -H "Content-Type: application/json" \
  -d '{ "name": "order.paid", "schema": { "amount": "number", "order_id": "string" } }'
```

**Node.js**

```ts
await hatang.events.create({ name: "order.paid", schema: { amount: "number", order_id: "string" } });
```

**Python**

```python
requests.post(
    "https://api.hatang.dev/events",
    headers={"Authorization": f"Bearer {os.environ['HATANG_API_KEY']}"},
    json={"name": "order.paid", "schema": {"amount": "number", "order_id": "string"}},
)
```

**PHP**

```php
$client->post('/events', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('HATANG_API_KEY')],
    'json' => ['name' => 'order.paid', 'schema' => ['amount' => 'number', 'order_id' => 'string']],
]);
```

- Event names use letters, digits and `.`, `_`, `:`, `-`, up to 100 characters.
- Names starting with `hatang:` or `resend:` are reserved for built-in events. Today there is one: `hatang:contact.created`, fired when a contact is created through the API, the dashboard or a CSV import.

## 2. Send events from your app

Send an event for a contact by `contact_id`, or by `email`. With `email`, Ha Tang creates the contact if it does not exist yet.

**cURL**

```bash
curl -X POST https://api.hatang.dev/events/send \
  -H "Authorization: Bearer $HATANG_API_KEY" -H "Content-Type: application/json" \
  -d '{ "event": "order.paid", "email": "lan@example.net", "payload": { "amount": 2000000, "order_id": "DH1024" } }'
```

**Node.js**

```ts
const { data } = await hatang.events.send({
  event: "order.paid",
  email: "lan@example.net",
  payload: { amount: 2000000, order_id: "DH1024" },
});
console.log(data.runs_started, data.runs_resumed);
```

**Python**

```python
hatang.events.send("order.paid", email="lan@example.net", payload={"amount": 2000000, "order_id": "DH1024"})
```

**PHP**

```php
$client->post('/events/send', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('HATANG_API_KEY')],
    'json' => ['event' => 'order.paid', 'email' => 'lan@example.net', 'payload' => ['amount' => 2000000, 'order_id' => 'DH1024']],
]);
```

The answer is `202`:

```json
{ "object": "event", "event": "order.paid", "contact_id": "con_0199e6c17a2b7f009c1e3f6a2b1d9e10", "runs_started": 1, "runs_resumed": 0 }
```

`runs_started` counts the enabled automations this event started for the contact; `runs_resumed` counts the runs that were waiting for it.

| Error | Cause |
|---|---|
| `422` "Unknown event" | The event is not defined. Create it with `POST /events` first. |
| `422` "must be a number" (or boolean, date) | A payload field does not match the schema. Fields not in the schema pass through unchanged. |
| `422` "Pass contact_id or email" | The request names no contact. |

## 3. Build the flow

Open **Email → Automations**, create an automation and:

1. Choose the event that starts it (**Start from an event**).
2. Click **+** under a step to add the next one.
3. Configure the selected step in the panel on the right.
4. **Save**, then **Enable**.

| Step | Configuration | Branches |
|---|---|---|
| `trigger` | `event_name` | |
| `send_email` | `template.id` (a **published** template), optional `from` and `subject` overriding the template, `template.variables` | |
| `delay` | `duration`, for example `"30 minutes"`, `"2 days"`, `"1 week"`. At most 30 days. | |
| `wait_for_event` | `event_name`, `timeout` (default `"30 days"`), optional `filter_rule` on the incoming event | `event_received`, `timeout` |
| `condition` | `rule` | `condition_met`, `condition_not_met` |
| `update_contact` | `properties` (declared properties), `unsubscribed` | |
| `add_to_segment` | `segment_id` | |

When a `send_email` step runs:

- Template variables come from the step's `template.variables` first, then from the event payload, then from the contact (`contact.first_name`, `contact.<property>`…).
- `{{{UNSUBSCRIBE_URL}}}` points to the contact's preferences page.
- A contact who unsubscribed from everything is skipped (the step is marked `skipped`).
- Each email is tagged `automation_id` and appears under **Email → Emails** like any other email.

Ha Tang refuses to enable an automation without a trigger, with a template that is not published, or with a `send_email` step that has no `From` address in the step or the template.

### Conditions

A `rule` compares one field with a value:

```json
{ "field": "event.amount", "operator": "gte", "value": 1000000 }
```

- **Fields**:
  - `event.*`: the payload of the event that started the run.
  - `contact.*`: `email`, `first_name`, `last_name`, `unsubscribed` and custom properties.
  - `wait_events.<event name>.*`: the payload received by a `wait_for_event` step.
- **Operators**: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `contains`, `starts_with`, `ends_with`, `exists`, `is_empty`.
- **Compare with another field**: `"value": { "var": "contact.limit" }`.
- **Combine rules**: `{ "type": "and", "rules": [ … ] }` or `{ "type": "or", "rules": [ … ] }`.

`filter_rule` on `wait_for_event` uses the same shape, with `event.*` being the incoming event. A waiting run only resumes for an event that passes the filter.

### Create with the API

```bash
curl -X POST https://api.hatang.dev/automations \
  -H "Authorization: Bearer $HATANG_API_KEY" -H "Content-Type: application/json" \
  -d '{
  "name": "After payment",
  "status": "enabled",
  "steps": [
    { "key": "start", "type": "trigger", "config": { "event_name": "order.paid" } },
    { "key": "thanks", "type": "send_email", "config": { "template": { "id": "order-confirmed" } } },
    { "key": "big", "type": "condition", "config": { "rule": { "field": "event.amount", "operator": "gte", "value": 1000000 } } },
    { "key": "vip", "type": "add_to_segment", "config": { "segment_id": "aud_0199e6c17a2b7f009c1e3f6a2b1d9e10" } },
    { "key": "review", "type": "wait_for_event", "config": { "event_name": "review.left", "timeout": "3 days" } },
    { "key": "remind", "type": "send_email", "config": { "template": { "id": "review-reminder" } } }
  ],
  "connections": [
    { "from": "start", "to": "thanks" },
    { "from": "thanks", "to": "big" },
    { "from": "big", "to": "vip", "type": "condition_met" },
    { "from": "big", "to": "review", "type": "condition_not_met" },
    { "from": "review", "to": "remind", "type": "timeout" }
  ]
}'
# { "object": "automation", "id": "auto_…" }
```

- An automation has exactly one `trigger` step, at most 100 steps and 200 connections.
- Step keys are unique, with letters, digits, `-` and `_`, up to 64 characters.
- Nothing may lead back to the trigger.

## Enable, edit, stop

- The steps of an **enabled** automation cannot change. Disable it to edit it, or **Duplicate** it into a new one.
- A run keeps the version of the automation it started with, even if you edit the automation later.
- `POST /automations/{id}/stop` disables the automation and cancels every run that has not finished.
- `POST /automations/{id}/duplicate` creates a disabled copy.

## Runs

Each time a contact enters an automation, Ha Tang creates a **run**. The **Runs** tab lists them by status:

| Status | Meaning |
|---|---|
| `running` | Executing steps. |
| `waiting` | Waiting for a `delay` to end or for an event. |
| `completed` | Reached the end of the flow. |
| `failed` | A step failed; the run shows the error. |
| `cancelled` | Stopped, for example by `POST /automations/{id}/stop`. |

Open a run to see each step: when it ran, its result (for example the id of the email it sent) and any error.

```bash
curl "https://api.hatang.dev/automations/auto_…/runs?status=failed,waiting" -H "Authorization: Bearer $HATANG_API_KEY"
curl "https://api.hatang.dev/automations/auto_…/runs/arun_…" -H "Authorization: Bearer $HATANG_API_KEY"
```

The flow editor has a **Send a test event** box: enter an email address and a payload to run an enabled automation for that contact.

## SDK

```ts
import { Hatang } from "@hatang/sdk";

const hatang = new Hatang(process.env.HATANG_API_KEY, { baseUrl: "https://api.hatang.dev" });

await hatang.events.create({ name: "order.paid", schema: { amount: "number" } });
await hatang.events.send({ event: "order.paid", email: "lan@example.net", payload: { amount: 2000000 } });
const { data } = await hatang.automations.runs.list("auto_…", { status: "failed" });
await hatang.automations.stop("auto_…");
```

## API reference

| Endpoint | Description |
|---|---|
| `POST /events`, `GET /events`, `GET`, `PATCH`, `DELETE /events/{id or name}` | Event definitions. `PATCH` changes the schema. |
| `POST /events/send` | Send an event for a contact. |
| `POST /automations`, `GET /automations`, `GET`, `PATCH`, `DELETE /automations/{id}` | Automations. |
| `POST /automations/{id}/stop`, `POST /automations/{id}/duplicate` | Stop, duplicate. |
| `GET /automations/{id}/runs`, `GET /automations/{id}/runs/{run_id}` | Runs (`status`, `limit`, `after`) and one run with its steps. |

## Next steps

- [Templates](https://hatang.dev/docs/guides/email/templates.md)
- [Contacts, segments and topics](https://hatang.dev/docs/guides/email/audiences.md)
- [Metrics and logs](https://hatang.dev/docs/guides/email/metrics-and-logs.md)

Source: https://hatang.dev/docs/guides/email/automations
