# Working with Plainsend (for agents)

Plainsend is a headless marketing platform: contacts with consent, email templates, journeys and broadcasts, all
driven by API, CLI (`plainsend`) or MCP. You work like a developer: draft, check, then ask a person to approve what
reaches customers.

## Your key

`workspace_get` says which account you act on and your key's kind. In a new workspace, start with `setup_status`:
the first-run checklist (test to yourself, business details, plan, domain, contacts, a starter journey, the first
approved send), each step with its state, what is missing, the tools that do it and whether only a person can. An **agent** key may read everything (except raw
source rows), draft brands, templates, sources and journeys, preview, simulate, preflight, test-send and import data.
It may not erase contacts, change settings, add sending domains or manage keys (refused with `forbidden` and a fix),
and **activating a journey or sending a broadcast becomes an approval**: you get
`{ approvalRequired: true, approvalId, consoleUrl }`, an owner approves it (console "Needs you", or
`plainsend approvals approve <id>`), and `approval_status` tells you the outcome. Never claim something went live
until `approval_status` says `approved`. An owner approves a request part by part (a broadcast's audience, message
and send settings; a journey's entry, steps and goal; checks run by themselves). If you change something after its
part was approved (a new template version, a new draft), that part resets and the owner sees what you changed:
say so when you change it. `log_search` shows who did what, people and agents alike.

## Marketing as code

Keep the account's resources as files in the customer's repo and apply them, so every change is reviewable:

```
plainsend/
  settings.yaml                (tracking, utm, frequencyCap: owner keys only)
  domains.yaml                 (a list of {domain, brand?}: owner keys only)
  brands/<key>.yaml            brand_put       (name, fromEmail, fromName, postalAddress, subscriptionTypes)
  sources/<key>.yaml           source_put      (connector, mapping or mappingFile, consentSource, identity,
                                                credentialsFromEnv: {field: SECRET_NAME})
  blocks/<key>.mjml|.yaml      block_put
  brandkits/<brand>.yaml       brandkit_put
  templates/<key>.mjml|.html   template_put    (YAML front matter: brand, subject, textBody?)
  segments/<key>.yaml          (the segment document)
  journeys/<key>.yaml          journey_put     (saves a draft; activation is separate)
```

The folder is one **workspace bundle**: `workspace_plan` shows, in dependency order, what applying it would create,
change or leave alone, with each resource's diff and issues (references inside the bundle resolve before the resources
exist). `workspace_apply` saves everything as drafts, stops at the first error with what was and was not applied, and
asks an owner for one **go-live** approval (a part per journey, and the first broadcast if the bundle has one).
`workspace_export` returns the workspace as a bundle without secrets. `plainsend workspace plan|apply|export [dir]`
does the same from a terminal (`plainsend plan|apply <dir>` too). Saving unchanged content is a no-op, so applying
twice is safe.

**Gallery packs** are ready-made bundles (clinic starter, win-back, monthly newsletter, tax time, software trial to
paid). `gallery_search` lists them (category, industry, words); `gallery_show` checks one against the account (which
contact properties and events it has or lacks, and how to provide each) and previews every template in the brand's
kit; `gallery_install` applies it as drafts with keys the account already uses renamed (`welcome` → `clinic-welcome`),
and its journeys go live through the same go-live approval. Pass `plan: true` to see the workspace plan first.

## The loop

1. **Data**: for a sheet, `source_upload` (columns and suggested choices), `source_upload_plan` with the choices
   (a dry run: counts and rejected rows with fixes), then `source_upload_import`. Or `source_put` a files source with a
   mapping (plainsend://docs/mappings) and `source_import_file`. `source_quarantine` lists rejected rows. Consent needs
   the owner's word: a column they confirmed, or their statement of how people agreed; never invent one. A CRM is
   connected by a person: give them the `source_connect_link`.
2. **Content**: `template_lint` (fix every error; read the warnings), `template_put`, then `template_preview` as a real
   contact (plainsend://docs/templates).
3. **Journey**: `journey_validate`, `journey_put`, `journey_plan`, `journey_simulate` (plainsend://docs/journeys).
4. **Ask**: `journey_request_activation` or, for a one-off send, `send_preflight` then `send_request_broadcast`.
5. **Watch**: `journey_funnel`, `enrolment_trace`, `report_sends`, `report_health`, `contact_get` (consent and recent
   skips: why someone did not get an email).
6. **Outcomes**: `goal_put` makes an event a goal (default: the last human click within 7 days gets the credit);
   `report_attribution` says what emails led to, per campaign, content, journey step or broadcast. Quote its `rule`
   next to every number you report.

## Rules that never bend

- Every message goes through the eligibility check (consent, suppression, frequency cap, sandbox allowlist, verified sending domain,
  the reputation guard). No key can bypass it; a refusal is a **skip** with a reason, not an error. A new self-serve
  workspace is in the sandbox: it sends only to its own members (`sandbox_team_only` otherwise) until it has paid and
  verified a sending domain. Until its brand has a real postal address, it sends only tests and mail to its members
  (`postal_address_missing` otherwise).
- A new sending domain warms up: a daily cap rising from 50 over about three weeks (`domain_warmup`). Over the cap a
  message **waits** for a later day (most engaged contacts first), it is not skipped; preflight's `warm_up` step says
  how many. Big broadcasts send a first slice and hold the rest until its bounces are in.
- The reputation guard pauses a brand whose bounce or complaint rate crosses the pause line (`report_health` shows the
  rates, lines and why). Its messages skip with `reputation_paused` until an owner resumes it
  (`reputation_request_resume` asks one).
- Plans have limits (`billing_status`: plan, usage this month, limits). On Solo, or an account that is not paid up, new
  sends stop at 120% of the plan's monthly emails and skip with `plan_email_limit` (preflight's `plan_limit` step says
  how many); elsewhere going over only warns. Unsubscribed contacts are free. You cannot buy or change a plan: say
  what is needed and an owner does it in Settings → Billing.
- Consent comes from data with evidence (a mapping's `consent`, the identify API), never from a guess.
- Errors are `{ code, message, details, fix }`: read the fix and correct your input rather than retrying blindly.
