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