# Pharos Automations

Multi-step email sequences fired by per-project triggers and timed by
QStash. No cron, no daemon — when a contact enters an automation, each
step's delay becomes a delayed QStash message that POSTs back to Pharos
when its time comes.

## Concepts

**Automation** — a per-project sequence of email steps with a single
*trigger* that decides when a contact gets enrolled.

**Step** — one email send, with an optional `skipIfTag` condition.
Steps run in order; each step's `delayMinutes` is the wait *before* it
fires (relative to the previous step completing).

**Run** — one row per `(contact, automation)` enrollment. It tracks the
current step pointer, when the next step fires, and a JSON history of
every step event. Re-enrollment is only allowed once a previous run
finishes (status `sent` or `failed`).

## Triggers (v1)

| Trigger          | Fires when                                                    |
|------------------|---------------------------------------------------------------|
| `new_subscriber` | A contact lands on any list (`/api/subscribe`).               |
| `tag_added`      | A tag is added to a contact (via PATCH or `/api/contacts/event`). Optional `triggerConfig.tag` filters to one tag; empty matches all. |

`schedule` and `no_open` exist in the schema but aren't wired in v1.

## Step shape

| Field          | Notes                                                                 |
|----------------|-----------------------------------------------------------------------|
| `delayMinutes` | Wait before this step fires. `0` = immediate. Common: 0, 5, 60, 1440 (1d), 4320 (3d), 10080 (1w), 43200 (30d). |
| `templateId`   | One of `welcome`, `newsletter`, `announcement`. Picks the React Email shell. |
| `subject` / `body` | English content. Body is plain text injected into the template. |
| `subjectPt` / `bodyPt` | Optional Brazilian Portuguese variants. Contacts with `language = pt-BR` get this; others fall back to EN. |
| `skipIfTag`    | If the contact has this tag at processor time, skip the send (no email goes out) and move to the next step. |

## Lighting beacons: skipIfTag and `/api/contacts/event`

Pharos doesn't know what "paid" or "onboarded" means in your domain. It
checks tags. External systems (Stripe webhooks, your app's backend,
Linear, anything) light a beacon by tagging contacts via:

```
POST /api/contacts/event?secret=<WEBHOOK_SECRET>
{
  "projectId": "my-app",
  "email": "user@example.com",
  "tag": "paid",
  "action": "add"          // or "remove"; defaults to "add"
}
```

On `add`, Pharos also fires `tag_added` automations for that tag — so a
single webhook call can both *suppress an in-flight chain step* (via
`skipIfTag`) and *enroll the contact in a different chain*.

`WEBHOOK_SECRET` is shared with the SES webhook. If you didn't set it,
the endpoint is unguarded — only safe behind a private network.

## Editing while runs are in flight

- Pharos stores `currentStepId`, not `currentOrder`. So if you reorder
  steps, in-flight contacts stay on whichever step they were waiting
  for.
- If you **delete** the step a contact was waiting on, that run ends
  gracefully (no email, no error).
- If you **edit copy** on a step that hasn't fired yet for some
  contacts, they get the new copy when their wait elapses. No snapshot.
- Toggling **Paused** stops in-flight runs at the next processor tick;
  they get marked `skipped`.

## Example: Welcome drip

Three-step linear drip after subscribe.

```
Trigger: new_subscriber

Step 1 — Immediately
  Template: welcome
  Subject: "Welcome to My App 👋"
  Body:    "Quick start: log your first meal..."

Step 2 — 1 day later
  Template: newsletter
  Subject: "Day 1 — what to track"
  Body:    "Three things that help on day one..."

Step 3 — 3 days later
  Template: announcement
  Subject: "Already love it? Tell a friend"
```

Bilingual: each step has `subjectPt` / `bodyPt` filled. Contacts with
`language=pt-BR` get the Portuguese copy automatically.

## Example: Payment nudge with skipIfTag

Subscribe → free trial. After 3 days, nudge if not paid. After 7 more,
last call. As soon as they pay, both nudges stop firing.

```
Trigger: new_subscriber

Step 1 — Immediately
  welcome / "Welcome to your trial"

Step 2 — 3 days later
  newsletter / "How's it going so far?"
  skipIfTag: paid

Step 3 — 4 days later
  announcement / "Last chance to keep your data"
  skipIfTag: paid
```

Then on checkout success:

```bash
curl -X POST 'https://pharos.d2vsolutions.com/api/contacts/event?secret=...' \
  -H 'content-type: application/json' \
  -d '{"projectId":"my-app","email":"user@x.com","tag":"paid"}'
```

Step 2's processor (or step 3's, whenever next fires) sees the `paid`
tag, marks itself skipped in the run history, schedules nothing. Done.

## Example: Post-purchase onboarding

When someone is *added* with the `paid` tag (e.g. they bought right
away), enroll them in a different sequence than the trial users.

```
Trigger: tag_added
Trigger config: { "tag": "paid" }

Step 1 — Immediately
  welcome / "Thanks for going pro!"

Step 2 — 1 day later
  newsletter / "Three pro features worth trying first"

Step 3 — 7 days later
  newsletter / "Save 20% on your annual upgrade"
  skipIfTag: pro_annual
```

Same `/api/contacts/event` call adds `paid`; Pharos sees the new tag
and fires the matching automation. If they upgrade to annual, tag them
`pro_annual` and step 3 quietly skips.

## Example: Tag-based segment broadcast

Tag a contact `vip` from anywhere; Pharos sends them a one-step
sequence that's effectively a personalized broadcast.

```
Trigger: tag_added
Trigger config: { "tag": "vip" }

Step 1 — Immediately
  announcement / "You're on the VIP list — early access opens Friday"
```

## Run history

Every automation's edit page shows recent runs at the bottom: contact
email, status, current step (for pending), expandable step trace with
timestamps. Filter by status. Failed runs surface the error inline.

## Operational notes

- **Failures**: a step throws → the run is marked `failed`, the chain
  stops, no auto-retry. Check the run history to see which step blew up
  and why.
- **Disabled automation**: when a step's processor runs and finds the
  automation disabled, it marks the run `skipped` and stops. Re-enabling
  doesn't resume in-flight runs (they stay skipped); new triggers
  create fresh enrollments.
- **Quotas**: QStash free tier covers ~500 messages/day. Each step =
  one message. With 50 users entering a 3-step drip on the same day
  that's 150 messages. Plenty of headroom.
- **Contact deletion**: deleting a contact does NOT cancel their
  in-flight runs. The processor will harmlessly fail to look them up
  next tick. Worth cleaning up later if it becomes noise.
