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
- Data: for a sheet,
source_upload(columns and suggested choices),source_upload_planwith the choices (a dry run: counts and rejected rows with fixes), thensource_upload_import. Orsource_puta files source with a mapping (plainsend://docs/mappings) andsource_import_file.source_quarantinelists 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 thesource_connect_link. - Content:
template_lint(fix every error; read the warnings),template_put, thentemplate_previewas a real contact (plainsend://docs/templates). - Journey:
journey_validate,journey_put,journey_plan,journey_simulate(plainsend://docs/journeys). - Ask:
journey_request_activationor, for a one-off send,send_preflightthensend_request_broadcast. - Watch:
journey_funnel,enrolment_trace,report_sends,report_health,contact_get(consent and recent skips: why someone did not get an email). - Outcomes:
goal_putmakes an event a goal (default: the last human click within 7 days gets the credit);report_attributionsays what emails led to, per campaign, content, journey step or broadcast. Quote itsrulenext 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_onlyotherwise) 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_missingotherwise). - 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'swarm_upstep 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_healthshows the rates, lines and why). Its messages skip withreputation_pauseduntil an owner resumes it (reputation_request_resumeasks 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 withplan_email_limit(preflight'splan_limitstep 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.