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 for the emails the automation sends, with a
Fromaddress 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
- Node.js
- Python
- PHP
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" } }'
await hatang.events.create({ name: "order.paid", schema: { amount: "number", order_id: "string" } });
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"}},
)
$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:orresend: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
- Node.js
- Python
- PHP
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": "[email protected]", "payload": { "amount": 2000000, "order_id": "DH1024" } }'
const { data } = await hatang.events.send({
event: "order.paid",
payload: { amount: 2000000, order_id: "DH1024" },
});
console.log(data.runs_started, data.runs_resumed);
hatang.events.send("order.paid", email="[email protected]", payload={"amount": 2000000, "order_id": "DH1024"})
$client->post('/events/send', [
'headers' => ['Authorization' => 'Bearer ' . getenv('HATANG_API_KEY')],
'json' => ['event' => 'order.paid', 'email' => '[email protected]', 'payload' => ['amount' => 2000000, 'order_id' => 'DH1024']],
]);
The answer is 202:
{ "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:
- Choose the event that starts it (Start from an event).
- Click + under a step to add the next one.
- Configure the selected step in the panel on the right.
- 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.variablesfirst, 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_idand 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:
{ "field": "event.amount", "operator": "gte", "value": 1000000 }
- Fields:
event.*: the payload of the event that started the run.contact.*:email,first_name,last_name,unsubscribedand custom properties.wait_events.<event name>.*: the payload received by await_for_eventstep.
- 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
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
triggerstep, 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}/stopdisables the automation and cancels every run that has not finished.POST /automations/{id}/duplicatecreates 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.
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
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: "[email protected]", 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. |