Developers People CRM

Platform

People CRM

Manage contacts, groups, stages, rules, imports, and campaign audiences through the CLI, API, and site agent.

MCP access

The existing People MCP read/write tools use these shared operations with separate CRM consent, site eligibility and role checks. Source-only grants cannot access contacts. See the MCP setup and tool guide for availability, scope and disconnect.

One contact record. Many groups. Clear progress.

People keeps a business’s contacts, notes, follow-ups, and history together. Groups organize contacts; optional stages track progress inside each group. Rules can update both and send a saved email or series. One-off campaigns stay in Emails.

Get started

Use the current frontpage-host CLI. Sign in, then choose a site using --site on each command or frontpage use YOUR_SITE. A site is its subdomain slug, such as acme, not a full URL or UUID.

npm install -g frontpage-host@latest
frontpage login
frontpage people overview --site YOUR_SITE --json
frontpage people sources --site YOUR_SITE --json
frontpage people list --site YOUR_SITE --limit 50 --json

Replace YOUR_SITE and all uppercase ID placeholders before running an example. Copy IDs from real results: groups, stages, people, rules, templates, and series use UUIDs. Forms use a stable form slug; products use their item ID. Do not invent IDs.

Access stays scoped to your site.

People requires Growth or above. Public API and CLI access requires Scale, Enterprise, or Internal, plus membership in the chosen site. Viewers can read; editors and admins can change data. A request cannot select another tenant through its JSON body.

--json returns structured output. Complex data accepts --file payload.json or --data 'JSON'. Use frontpage help people and frontpage help mail for command flags.

Contacts, notes, and history

Each contact needs a valid email or phone. A name is optional. If you supply both identifiers, both must be valid. Matching email or phone updates the existing contact; conflicting identities fail instead of merging two people.

frontpage people add --email alex@example.com --name "Alex Rivera" --site YOUR_SITE --json
frontpage people get PERSON_ID --site YOUR_SITE --json
frontpage people set PERSON_ID --name "Alex Rivera" --phone +12025550166 --site YOUR_SITE --json
frontpage people set PERSON_ID --address-name "Alex Rivera" --line1 "12 Main St" --line2 "Suite 4" --city Austin --region TX --postal 78701 --country US --site YOUR_SITE --json
frontpage people note PERSON_ID --body "Asked for a quote. Call on Friday." --site YOUR_SITE --json
frontpage people follow-up PERSON_ID --at 2026-11-06T15:00:00Z --site YOUR_SITE --json
frontpage people list --follow-up due --site YOUR_SITE --json
frontpage people hide PERSON_ID --site YOUR_SITE --json
frontpage people unhide PERSON_ID --site YOUR_SITE --json
  • Details: name, email, phone, mailing address, original source, first seen, and last seen. Address fields include recipient name, line 1, line 2, city, region, postal code, and country.
  • Notes: people note PERSON_ID --body "…" adds a manual note. people rm-note PERSON_ID --event NOTE_EVENT_ID removes an owner’s manual note. Source submissions and payments are not removable notes.
  • Follow-up: set an ISO date with --at; clear it with people follow-up PERSON_ID --clear. The person page highlights overdue dates.
  • Activity: people get returns the recent 50 events. Use person.activity for paged CRM and email history. The person page shows populated categories with loaded counts. Group and stage pages do not show a separate activity feed.
  • Hide and restore: hide is recoverable. It retains identity, history, and membership. It excludes the person from campaign audiences and automated emails. Later source ingest may restore them.

Find and export people

People list and detail reads include eligible and reasonCode. The UI shows Can email or Can't email from the same policy used by the email picker. Codes are allowed, no_active_email, delivery_restricted, unsubscribed, not_allowed, and permission_missing. Hidden or missing/invalid email addresses use no_active_email. These read fields do not grant consent or promise sender readiness, budget, or delivery. API, people list --json, people get --json, list_people, and get_person return the same fields. CSV includes groups and stages instead of global status and tags.

people list and people export accept search, group, stage, source, form, follow-up, hidden state, sort and recency. List defaults to 50 contacts, supports up to 200 per page, and returns nextCursor. Pass that cursor to --cursor to continue. Export writes UTF-8 CSV; it is a contact export, not a complete activity archive.

Groups: organize people

In the workspace, open People CRM, then choose the People or Groups tab. Group details has Group details, Stages and Rules tabs. Use Email on Group details for joining emails. On a stage, Manage rules lets you create a rule, place an existing rule, or attach an email. Placement changes where a rule appears; its Start when event still controls execution. New attachments save paused.

A person can belong to several groups at once. Use a group for Leads, Customers, Newsletter, or another useful list. Rename a group freely: its ID stays the same.

Add people yourself or with rules

A manual group keeps membership until you, the agent, an import, or a rule changes it. These commands also manage the groups shown in the People filter.

frontpage people groups create --label Leads --color sky --mode manual --site YOUR_SITE --json
frontpage people groups list --site YOUR_SITE --json
frontpage people groups update --id GROUP_ID --label "New enquiries" --site YOUR_SITE --json
frontpage people membership add --person PERSON_ID --group GROUP_ID --site YOUR_SITE --json
frontpage people membership get --person PERSON_ID --site YOUR_SITE --json
frontpage people list --group GROUP_ID --site YOUR_SITE --json
frontpage people export --group GROUP_ID --out leads.csv --site YOUR_SITE
frontpage people groups archive --id GROUP_ID --site YOUR_SITE --json
frontpage people groups restore --id GROUP_ID --site YOUR_SITE --json

Before a reviewed groups.update, read groups.list and pass its saved emailVersion as expectedGroup through --data or --file. Email tasks use this full saved-configuration check. A changed name, color, mode, conditions or archive state returns 409 before writes; membership counts do not change the version. Read and review the latest settings before retrying. The check is optional for existing callers.

Independent edits can pass expectedFields through JSON with saved values for label, color, mode, conditions, or archivedAt. Only supplied fields are compared. Omitted update fields keep their current saved values. A stale comparison returns 409 without writing. Read and review saved values before submitting a revised edit.

Select people from a manual group

The group header offers Add people for manual groups and View matching people for conditional groups. Membership selection includes visible phone-only contacts and people without marketing permission. Current members show Already in this group and cannot be selected.

groups.people accepts groupId, optional q, source, sort (newest, oldest, name), page, selectedOnly, selected IDs and availability (all, available, member). Availability filters saved membership before counting and paging; it never filters by email permission. It returns 20 people per page, the full filtered total, actual page, phone/email details and member. It rejects conditional or archived groups and never records consent.

frontpage people workspace groups.people --group GROUP_ID --data '{"page":1,"q":"Alex"}' --site YOUR_SITE --json
frontpage people workspace membership.addMany --file selected-people.json --site YOUR_SITE --json

selected-people.json contains {"groupId":"GROUP_ID","personIds":["PERSON_ID"]}. membership.addMany accepts 1 to 20 IDs per request. The UI selects up to 100 and submits batches of 20. Results include added, alreadyMember, skipped, failed and per-person results with personId, status and an optional message. Keep failed or unconfirmed IDs for retry. Current members keep their stages and do not receive duplicate join events. Hidden people are skipped. A group changed to conditional or archived cannot receive additions through this operation. Successful additions save immediately and use normal active entry rules. They do not grant email permission or change other groups.

groups.count accepts a saved group id or explicit conditions. It checks all visible people in one consistent read and returns matched, checked, unknown, complete and sendsNothing:true. A known purchase count of zero does not match last-purchase date comparisons; it is not missing information or a purchase today. Use purchase count equal to zero for people who have never purchased. Actual unavailable facts remain unknown. A failed read is an error, never zero. The workspace shows this count under the condition editor. Saved group members open in the People tab with the group selected. The count describes the proposed conditions; the People list shows saved membership.

Create the group with a name and complete conditions if Match conditions is selected. Existing groups have no page-wide Save group button. Names save when editing finishes; colors save on selection. Edit How people join in its own modal and click Apply changes to save membership settings. X, Cancel, and Escape discard unapplied membership edits and close immediately without another prompt. The backdrop does not close the modal. Activate and Pause save immediately. Emails, stages, and rules save independently. Use a stable requestKey with groups.create or groups.update and retry the exact original key and payload after a lost response.

groups.preview accepts limit (1 to 200, default 200) and cursor, returning checked, unknown, matches and nextCursor. Match rows include phone and email. A batch may have no matches while more people remain. Counts describe people checked, not saved membership. With a saved group ID, pass expectedConditions to reject a condition or mode change between pages. Explicit conditions previews edits without saving them. Preview changes no membership and sends no email.

Before applying membership changes

To Match conditions: “Membership will update to match these conditions. Some people may join or leave.”

To Add people: “Current members will stay. This group will stop updating from conditions.”

Existing exceptions, saved pauses, stages and history stay saved. The UI shows this effect before Apply changes without another confirmation step.

Keep a group matched to conditions

An automatic group uses saved contact facts. The worker checks contacts in rotating batches and updates membership as those facts or time change. Preview first. Save this as repeat-buyers.json:

{
  "label": "Repeat buyers",
  "color": "emerald",
  "mode": "conditions",
  "conditions": {
    "all": [
      {
        "field": "purchaseCount",
        "op": "gte",
        "value": 2
      },
      {
        "field": "lastPurchaseDays",
        "op": "lte",
        "value": 90
      }
    ]
  }
}
frontpage people groups count --file repeat-buyers.json --site YOUR_SITE --json
frontpage people groups create --file repeat-buyers.json --site YOUR_SITE --json
frontpage people membership override --person PERSON_ID --group GROUP_ID --override include --site YOUR_SITE --json
frontpage people membership override --person PERSON_ID --group GROUP_ID --override exclude --site YOUR_SITE --json
frontpage people membership override --person PERSON_ID --group GROUP_ID --override auto --site YOUR_SITE --json
frontpage people membership hold --person PERSON_ID --group GROUP_ID --hold false --site YOUR_SITE --json

include keeps one person in an automatic group; exclude keeps them out. auto clears the exception and applies that group’s conditions immediately when the needed facts are available. It can add or remove membership and run the group’s normal rules. It does not change other groups or email permission. An earlier saved pause still prevents automatic changes.

Earlier pauses. New --hold true requests fail unless the person already has that pause; an existing pause remains unchanged. Use --hold false to resume a saved pause. This keeps membership, stage and exception, restarts the time-rule clock, and skips earlier pending work for this person/group. It fails with HTTP 409 while a group rule is running; try again after it finishes. Archived groups must be restored first. No saved pause is cleared automatically. Failed runs remain available for an explicit review and retry; resuming does not retry them.

Preserve existing records.

Groups and stages are the only membership and progress model. Global status/tag fields, filters, tools and commands are retired. Existing person IDs, groups, stages, memberships, notes, follow-ups and history remain saved. Reads never initialize or convert records. Use group and stage IDs.

Archive groups instead of deleting their history. Assignments stay saved; related rules pause. Restoring a group does not reactivate those rules. Review and activate them explicitly.

Stages: track progress within a group

Stages are optional. A person can have one current stage per group, or no stage. A simple funnel might use New → Working → Customer. Save the group before adding stages or their rules.

frontpage people stages create --group GROUP_ID --label New --color sky --position 0 --site YOUR_SITE --json
frontpage people stages create --group GROUP_ID --label Working --color yellow --position 1 --site YOUR_SITE --json
frontpage people stages create --group GROUP_ID --label Customer --color emerald --position 2 --terminal true --site YOUR_SITE --json
frontpage people membership stage --person PERSON_ID --group GROUP_ID --stage STAGE_ID --site YOUR_SITE --json
frontpage people membership advance --person PERSON_ID --group GROUP_ID --site YOUR_SITE --json
frontpage people stages reorder --data '{"groupId":"GROUP_ID","stageIds":["NEW_ID","WORKING_ID","CUSTOMER_ID"]}' --site YOUR_SITE --json
frontpage people membership stage --data '{"personId":"PERSON_ID","groupId":"GROUP_ID","stageId":null}' --site YOUR_SITE --json
frontpage people stages archive --id STAGE_ID --group GROUP_ID --site YOUR_SITE --json
frontpage people stages restore --id STAGE_ID --group GROUP_ID --site YOUR_SITE --json
  • Joining a group starts at its first stage when no previous stage exists. Rejoining keeps the prior stage; reset:true on add/move starts again.
  • membership.advance follows ordered nonterminal stages. It stops when the current stage is final. Choose a final stage explicitly with membership.stage or a rule’s set_stage action.
  • stageId:null clears the stage while keeping group membership.
  • stages.reorder requires every active stage once. A duplicate, incomplete, or stale order fails without partially changing positions.
  • Archiving a stage keeps its assignments and history and pauses related rules. Restoring it leaves those rules paused.

Rules: When / If / Then

Rules live beside their group or stage. Each has a start event, optional checks, and ordered actions. The editor saves new rules paused. Activate only after reviewing the result; activation applies to future events, not past submissions.

Example: follow up on an enquiry

Use a form slug from people sources. Save this as enquiry-rule.json:

{
  "name": "Follow up on contact enquiries",
  "trigger": {
    "event": "form.submitted",
    "formSlug": "contact",
    "owner": {
      "groupId": "GROUP_ID"
    }
  },
  "conditions": {
    "field": "email",
    "op": "present"
  },
  "actions": [
    {
      "type": "add_group",
      "groupId": "GROUP_ID"
    },
    {
      "type": "set_follow_up",
      "minutes": 1440
    }
  ],
  "enabled": false
}
frontpage people rules validate --file enquiry-rule.json --site YOUR_SITE --json
frontpage people rules preview --file enquiry-rule.json --person PERSON_ID --site YOUR_SITE --json
frontpage people rules create --file enquiry-rule.json --site YOUR_SITE --json
frontpage people rules enable --id RULE_ID --site YOUR_SITE --json
frontpage people rules pause --id RULE_ID --site YOUR_SITE --json
frontpage people rules update --id RULE_ID --file enquiry-rule.json --site YOUR_SITE --json
frontpage people rules runs --rule RULE_ID --limit 50 --site YOUR_SITE --json
frontpage people rules retry --run RUN_ID --site YOUR_SITE --json
frontpage people rules archive --id RULE_ID --site YOUR_SITE --json

Organize rules under a group or stage

The stage table opens Manage rules without leaving the group. Create a new paused rule, place an existing rule here, or attach a saved email as a paused entry rule. Each workflow returns to the stage manager. The Rules tab lists group and stage rules together and filters by placement. It shows the real event and ordered actions.

rules.get, rules.list and overview return an opaque revision for each rule. Call rules.place with id, expectedRevision and owner (groupId and optional stageId) to change only its placement. owner:null restores placement inferred from the real trigger/action groups. This operation accepts no execution edits.

frontpage people rules get --id RULE_ID --site YOUR_SITE --json
frontpage people workspace rules.place --file placement.json --site YOUR_SITE --json

placement.json contains {"id":"RULE_ID","expectedRevision":"REVISION_FROM_READ","owner":{"groupId":"GROUP_ID","stageId":"STAGE_ID"}}.

Placement validates active same-site references and rejects stale revisions with HTTP 409. It preserves the real trigger, name, conditions, actions, status, execution version, activation time, errors, run receipts and scheduled work. Only trigger.owner and the edit timestamp change. Read again after a conflict or uncertain response before retrying. General rules.update accepts optional expectedRevision; existing callers can still omit it.

New UI rules and email attachments save paused. Activate them separately. Generic attachment calls still default to active when enabled is omitted, so integrations that need review must send enabled:false. The UI removes optional condition authoring but displays and preserves existing saved conditions; CLI and agent condition support remains available. Duration controls offer minutes, hours, days and weeks while keeping exact whole minutes, including saved non-round values.

trigger.owner only places the rule in the editor. It never limits who can start the event. A form rule can belong to a stage without requiring the form submission to contain a group ID. Use conditions to limit it to existing members. Use trigger.groupId and trigger.stageId for membership events themselves.

When: choose a real event

EventWhen it occursSelector
person.added / person.updatedA contact is added or edited.—
form.submittedA form receives a submission.formSlug optional
booking.requestedSomeone requests an appointment.—
signup.subscribed / signup.resubscribed / signup.unsubscribedA mailing-list subscription changes.—
payment.paid / payment.refundedA payment succeeds or is refunded.—
order.paid / order.refunded / order.fulfilledA store order is paid, refunded, or fulfilled.productId optional on order.paid
square_order.paidA Square order is paid.—
group.joined / group.leftA person enters or leaves a group.groupId required
stage.enteredA person enters a stage.groupId and stageId required
group.elapsedA person spends the chosen time in a group.groupId and delayMinutes required
stage.elapsedA person spends the chosen time in a stage.groupId, stageId, and delayMinutes required

Forms are detected from the current site’s module instances. Product choices come from active store items. Renaming a display label keeps the saved slug/item ID, so the rule keeps working. Deleting or replacing that ID requires updating the rule; validation rejects a missing source.

If: add only useful checks

Leave conditions null to run for every matching event. Use {all:[…]} when every check must match, or {any:[…]} when one is enough. Sets accept 1–20 children, up to four levels and 100 total clauses.

Field or shapeMeaningComparison
purchaseCountNumber of recorded paid order/payment events.is, is_not, gte, lte, present
spentCentsTotal amountCents from those paid events. Integer cents, not a decimal amount.is, is_not, gte, lte, present
lastPurchaseDaysWhole days since the latest recorded paid event.is, is_not, gte, lte, present
lastSeenDaysWhole days since the contact’s last CRM activity.is, is_not, gte, lte, present
email / phone / nameContact details. present needs no value.is, is_not, contains, present
formSlug + submittedWhether this person submitted the named form.submitted: true or false
groupId + member; stageId optionalWhether a person belongs to a group or stage.member: true or false
subscribedExisting advanced subscription condition. Send eligibility remains separate.is, is_not with boolean value

Numeric values are whole numbers from 0 to 1,000,000,000. Amounts use integer cents: 2500 means 25.00. Purchase facts count recorded paid order/payment events and sum their amounts; they are not net revenue after refunds and do not convert currencies. Last activity means CRM activity, not anonymous page views. Unknown facts do not remove a person from an automatic group.

The simple editor offers Purchases, Activity, and Contact details. Existing advanced conditions—including nested sets and membership checks—remain available through the CLI and agent and stay intact during UI edits. Email permission is handled separately before each send.

Then: perform ordered actions

Action typeFieldsResult
add_group / remove_groupgroupIdAdd to or remove from a manual group.
move_groupfromGroupId, groupIdMove between manual groups.
set_stagegroupId, stageId, fromStageIds?Set a stage; an optional guard limits eligible starting stages.
advance_stagegroupId, fromStageIds?Advance through nonterminal stages.
set_follow_upminutesSet a follow-up after this many minutes.
clear_follow_up—Clear the person’s follow-up.
send_templatetemplateIdSend a saved template if eligible.
start_seriesseriesIdEnroll in a series with saved steps. Off waits at step 1; On starts sending.
cancel_seriesseriesIdStop this group’s enrollment; requires a group/stage trigger.

A rule accepts 1–10 actions. Add/remove/move actions target manual groups; automatic groups use conditions and owner exceptions. fromStageIds prevents a late event from resetting someone who has moved further through the funnel.

Example: move a paid lead to Customer

{
  "name": "Mark paid leads as customers",
  "trigger": {
    "event": "order.paid",
    "owner": {
      "groupId": "GROUP_ID",
      "stageId": "CUSTOMER_ID"
    }
  },
  "conditions": {
    "groupId": "GROUP_ID",
    "member": true
  },
  "actions": [
    {
      "type": "set_stage",
      "groupId": "GROUP_ID",
      "stageId": "CUSTOMER_ID",
      "fromStageIds": [
        "NEW_ID",
        "WORKING_ID"
      ]
    }
  ],
  "enabled": false
}

Example: send a follow-up after two days

{
  "name": "Follow up after two days",
  "trigger": {
    "event": "stage.elapsed",
    "groupId": "GROUP_ID",
    "stageId": "WORKING_ID",
    "delayMinutes": 2880,
    "owner": {
      "groupId": "GROUP_ID",
      "stageId": "WORKING_ID"
    }
  },
  "conditions": null,
  "actions": [
    {
      "type": "send_template",
      "templateId": "TEMPLATE_ID"
    }
  ],
  "enabled": false
}

Timed rules use minutes: 2880 is two days. They run through the worker, not a browser timer. Use rules runs to inspect outcomes and rules retry to retry eligible failed work. Rule edits create a new version; changed or paused rules cannot replay old runs.

Attach an email to a group or stage

Use one action, Attach an email, then choose a saved email template or a series with saved steps. Off records waiting enrollment at step 1; turning On starts those people. New series start On; existing Off series stay Off when edited. Group email rows, group/stage rule cards, the rule editor and the attachment picker warn when a linked series is Off and offer Open series. A rule can stay Active while its series is Off: people wait for that series, and other rule actions still work. Attaching a series or activating its rule does not turn the series On. An attachment creates a normal editable rule on group entry or stage entry. Campaigns are one-off emails and cannot be selected for automation.

Save a reusable template as welcome.json:

{
  "name": "Welcome to our business",
  "subject": "Thanks for getting in touch",
  "preheader": "Here is what happens next.",
  "blocks": [
    {
      "type": "paragraph",
      "text": "Thanks for getting in touch. Our team will contact you soon."
    }
  ]
}
frontpage mail template create --file welcome.json --site YOUR_SITE --json
frontpage people attachments create --group GROUP_ID --email-kind template --email-id TEMPLATE_ID --enabled false --site YOUR_SITE --json
frontpage mail series create --name "Getting started" --site YOUR_SITE --json
frontpage mail series steps set SERIES_ID --step template=TEMPLATE_ID,delay=0 --site YOUR_SITE --json
frontpage mail series activate SERIES_ID --site YOUR_SITE --json
frontpage people attachments create --group GROUP_ID --stage STAGE_ID --email-kind series --email-id SERIES_ID --enabled false --site YOUR_SITE --json
frontpage people rules enable --id ATTACHMENT_RULE_ID --site YOUR_SITE --json

Copy the attachment’s returned rule ID before enabling it. Pass --enabled false to review it first. Omitting enabled on the API keeps its existing enabled-by-default behavior; the editor explicitly saves paused attachments.

  • A real group/stage reentry can send another template. Duplicate events and action retries do not send that same action twice.
  • An existing email/series enrollment does not restart when the person reenters.
  • Removing someone from a group does not automatically cancel a series. Add cancel_series to a group/stage rule when that is the intended behavior; it affects that group’s enrollment only.
  • Pause or archive the attachment rule to detach it. This does not erase its history.

See the Email guide for content blocks, template variables, series step delays, sender settings, and email limits.

Marketing email permission

Permission belongs to the email address and site. Groups and stages never grant it. A person who opts out stays in their groups and stages. Campaign selection, rules, direct-address sends, series and delivery enforce saved withdrawals and restrictions.

Defaults and explicit choices

Entry pointNew contactExisting contact
Owner UI, people add, public API POST /people, agent add_personallowed by default, recorded as an owner assertionOmitted choice preserves the saved decision. Explicit choices can fill unknown permission; add cannot reverse a recorded refusal, mailing-list unsubscribe or suppression.
CSV importAllowed by default; an explicit list-wide choice or row value can select otherwiseList-wide choices leave existing decisions alone. Row values can record permission, but cannot reverse a refusal, unsubscribe or suppression.
Forms, orders, bookings and account eventsUnknown unless a supported subscriber signup records agreementPreserve saved permission; purchases and signup timestamps are not email agreement.
Group, stage, name, phone, note and follow-up changesNo permission changeNo permission change. Changing an email does not transfer an old address's grant; a recorded refusal stays in effect.

The add choice is allowed, unknown, or not_allowed. API and agent callers use the same emailPermission string field. CLI callers use --email-permission. Recording allowed asserts that you have permission; it does not obtain permission from the person.

# New owner-added contacts default to permission given. Repeated adds preserve decisions.
frontpage people add --email alex@example.com --site YOUR_SITE --json
# Choose unknown when you do not have a recorded agreement:
frontpage people add --email pat@example.com --email-permission unknown --site YOUR_SITE --json
# Record refusal without enabling marketing:
frontpage people add --email sam@example.com --email-permission not_allowed --site YOUR_SITE --json
frontpage people membership get --person PERSON_ID --site YOUR_SITE --json
# Explicitly stop or record a new owner decision on the person:
frontpage people person permission --person PERSON_ID --allowed false --site YOUR_SITE --json
frontpage people person permission --person PERSON_ID --allowed true --site YOUR_SITE --json

membership.get reports eligibility and the reason mail cannot send. A missing/invalid email, hidden person, missing permission, recorded refusal, subscriber unsubscribe or suppression can block marketing. An active subscriber agreement can establish permission even without a separate CRM flag. A requested test copy can go to its test recipient without a recorded grant or a new subscription. Saved withdrawals, hidden state and suppression still apply. Transactional confirmations keep their own existing sending flow.

An explicit address outside People is an owner-asserted recipient and does not create a CRM permission record. If that address matches a saved person, the normal CRM eligibility check applies. Known subscribers' unsubscribe and saved suppressions also apply when no CRM person matches. Once queued, a known person's ID stays attached so a later address change, hide or withdrawal prevents delivery. A test is not a way to send a customer campaign.

New CRM decisions store state, exact email, source, actor and recording time in attrs.emailPermissionRecord; the boolean attrs.emailPermission stays compatible with existing callers. Historical grants without that record remain supported. Permission events preserve the decision history. Do not add a permission condition to every rule; the sender already checks it.

Repair reviewed historical permission without running rules

Review each person's current address, agreement evidence, saved withdrawals and restrictions first. This operation only fills missing permission. It never adds a person, changes their groups/stages, clears a restriction, or replays automation.

Save repair.json with real values:

{
  "personId": "PERSON_ID",
  "allowed": true,
  "backfill": true,
  "expectedEmail": "alex@example.com",
  "evidence": "Owner confirmed this person's marketing-email agreement on 2026-10-12",
  "requestKey": "UNIQUE_REPAIR_UUID"
}
frontpage people person permission --file repair.json --site YOUR_SITE --json
frontpage people membership get --person PERSON_ID --site YOUR_SITE --json

Reuse the same key and evidence after a lost response. A repeat returns duplicate:true without changing the decision again. If the person withdrew permission afterward, a retry does not restore it. A changed address, hidden person, existing decision, active subscriber agreement, unsubscribe or suppression refuses a new backfill. A new explicit owner decision uses the normal allowed operation instead; it still cannot reverse a subscriber unsubscribe or suppression.

For a reviewed cohort, submit at most 25 people in each bounded batch. Each row needs its own stable request key:

{
  "allowed": true,
  "backfill": true,
  "evidence": "Owner confirmed marketing-email permission for this reviewed cohort",
  "people": [
    { "personId": "PERSON_ID", "expectedEmail": "alex@example.com", "requestKey": "UNIQUE_REPAIR_UUID" }
  ]
}

Run the same person permission --file reviewed-batch.json command. Read every row in results: domain conflicts include error and status, while successful rows include emailPermission and duplicate. The batch can partly succeed; receipts make a retry safe. Malformed batch input fails before any permission writes. Infrastructure errors can leave completed receipts, so retry the exact batch rather than inventing new keys. This capability works through the public API and manage_people too; it has no company-only endpoint.

Import a customer list from CSV

CSV imports accept up to 5 MB and 10,000 contact rows. Quoted commas, quoted newlines, UTF-8 BOM, and CRLF are supported. Larger lists need separate files; the UI does not render an unbounded list of failures.

The UI offers only Add people groups as import destinations and sends manualGroupsOnly:true. CLI and agent callers can pass the same option to import.start. The saved job retains it. If a destination changes to Match conditions before processing, affected rows report an error instead of gaining an include exception. Completed rows stay saved. Restore Add people mode and retry the saved job, or preview a new import without that group. Existing accepted jobs and deliberate advanced imports keep their policy when the option is omitted.

1. Upload and map columns

Full name,Email address,Phone,Permission
Alex Rivera,alex@example.com,,
"Pat, Jr.",pat@example.com,+12025550166,yes
Phone customer,,+12025550199,

Map exact header names in mapping.json. Row values yes/true record an owner assertion; no/false record refusal. Top-level emailPermission chooses allowed, unknown or not_allowed for new contacts with no row value; omission defaults to allowed for new imports. Accepted imports keep their saved policy on retries. Row values take priority, duplicate rows keep negative values, and list-wide choices preserve existing decisions. Imports cannot reverse refusals, unsubscribe or suppression. Use a new request key when the list choice changes.

{
  "mapping": {
    "name": "Full name",
    "email": "Email address",
    "phone": "Phone",
    "emailPermission": "Permission"
  },
  "groupIds": [
    "GROUP_ID"
  ]
}

2. Review matching and errors

Preview returns real new/existing counts, normalized rows, fields that can fill blank saved details, duplicates, and errors. A name is optional; email or phone is required. Existing nonblank contact details stay saved. A repeated identity collapses into one contact; conflicting email/phone matches fail instead of merging different people.

ProblemHow to fix it
Invalid emailUse a complete email, or clear it when a valid phone exists.
Invalid phoneUse a valid full number, preferably with country code, or clear it when a valid email exists.
Email or phone missingProvide at least one contact identifier.
Conflicting identitiesCorrect the CSV or the saved contact. The importer will not merge two people.
Invalid permission or column mappingUse true/false or yes/no; map existing unique headers.

The editor groups failures by reason, shows five examples per page, and shows ten ready contacts per page. There is no error download. Fix rows in the editor or upload a corrected CSV. The API returns bounded results for the accepted file limit.

3. Start and follow the saved import

frontpage people import preview --csv customers.csv --file mapping.json --site YOUR_SITE --json
frontpage people import start --csv customers.csv --file mapping.json --request-key REQUEST_UUID --site YOUR_SITE --json
frontpage people import get --id IMPORT_ID --site YOUR_SITE --json
# After correcting a saved contact conflict, retry the same job:
frontpage people import retry --id IMPORT_ID --site YOUR_SITE --json

Choose a fresh request UUID for each new import. If a response is lost, repeat import.start with the same key and unchanged CSV/mapping/groups. It returns the existing job. Reusing a key for different contents returns a conflict.

The worker saves its cursor and each row’s outcome, so closing the editor does not stop the job. Use import.get to inspect completion and failed rows. Correcting the CSV requires a new preview and new import key. import.retry retries failed rows from the original saved file after you fix the underlying saved contact conflict. Completed rows keep their receipts. Imports do not send emails or grant permission through group membership.

Email all people, a group, a stage, or selected people

The People button names the selected audience, such as Email all people or Email Newsletter. It opens a one-off draft in Emails. Drafting and sending stay there. Use a template creates an independent campaign draft; editing it never changes the source template.

Save audience.json:

{
  "audience": {
    "groupIds": [
      "GROUP_ID"
    ]
  }
}
AudienceJSON inside audience
All visible people{"filters":{}}
Groups{"groupIds":["GROUP_ID"]}
Stages{"stageIds":["STAGE_ID"]}
Selected people{"personIds":["PERSON_ID"]}
A filtered list{"filters":{"q":"Alex","followUp":"due"}}

ID lists combine as a union; filters narrow the result. Filters support q, source, formSlug, followUp, and lastSeenDays. An empty audience object is invalid; all people uses {"filters":{}} explicitly.

update.json uses the same saved email content shape shown above, with your campaign’s subject and blocks. Create it as an email draft, not a template:

frontpage mail email create --file update.json --name "Customer update" --site YOUR_SITE --json
frontpage mail campaign preview --file audience.json --site YOUR_SITE --json
frontpage mail campaign audience --id DRAFT_ID --file audience.json --site YOUR_SITE --json
# This command sends the campaign. Inspect the preview and draft first.
frontpage mail campaign send --id DRAFT_ID --request-key REQUEST_UUID --site YOUR_SITE --json
frontpage mail campaign get --id DRAFT_ID --site YOUR_SITE --json

Preview reports matched, eligible, no-email, suppressed, and permission-missing counts and sends nothing. Sending freezes content and eligible recipients, deduplicates email addresses, and uses the existing paced email queue. Reuse the send request key after a lost response. Check campaign get for queued, sent, pending, failed, and skipped work; queued does not mean delivered.

CLI, API, and agent reference

The editor, public API, CLI, and site agent call the same application service. Every operation below is available without opening the UI.

frontpage people workspace stages.reorder --file order.json --site YOUR_SITE --json
frontpage people rules create --data '{"name":"New leads","trigger":{"event":"person.added"},"conditions":null,"actions":[{"type":"set_follow_up","minutes":1440}],"enabled":false}' --site YOUR_SITE --json

Public API

POST /api/v1/sites/YOUR_SITE/people/workspace
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{"operation":"groups.create","label":"Leads","color":"sky","mode":"manual"}

The public workspace endpoint takes operation fields at the top level. The agent tool uses a payload object instead. Campaign operations use POST /api/v1/sites/YOUR_SITE/mail/campaign with operation set to preview, audience, send, or get. Existing contact endpoints remain available under /api/v1/sites/YOUR_SITE/people.

Site agent

Ask the agent to use get_people_docs, then manage_people. Its tool binds the current site and access rights; do not pass a site ID or slug. Example membership request:

{
  "operation": "membership.add",
  "payload": {
    "personId": "PERSON_ID",
    "groupId": "GROUP_ID"
  }
}

Example campaign preview:

{
  "area": "campaign",
  "operation": "preview",
  "payload": {
    "audience": {
      "stageIds": [
        "STAGE_ID"
      ]
    }
  }
}

The agent can create and edit saved templates and series with the existing Email tools, then attach their IDs. It can inspect runs, retry failures, update contacts, import lists, and manage group exceptions directly.

Workspace operation reference

OperationPayload fieldsPurpose
overview—Active groups, stages, rules, saved emails and series. Reads never initialize records.
sources—Supported events and current form/product IDs.
groups.listarchived?List groups; archived:true includes archived groups.
groups.getidOne group with its stages.
groups.createlabel, color?, mode?, conditions?, requestKey?Create a group. A stable requestKey makes an exact retry safe.
groups.updateid, changed fields, expectedGroup? or expectedFields?, requestKey?Edit a group without changing its ID. Preserve the original version and requestKey when retrying.
groups.archive / groups.restoreidArchive or restore a group. Related rules remain paused on restore.
groups.countid or conditionsCount all visible people against conditions without saving membership. Returns matched, checked, unknown, complete and sendsNothing.
groups.previewid or conditions; personId? or cursor?Check conditions without changing membership.
groups.peoplegroupId; q?, source?, sort?, page?, selectedOnly?, selected?Read the manual-group picker with contact details and current membership.
stages.creategroupId, label, color?, position?, terminal?Create a stage in a saved group.
stages.updateid, groupId, changed fieldsEdit a stage.
stages.reordergroupId, stageIdsSave every active stage ID in order, once each.
stages.archive / stages.restoreid, groupIdKeep assignments and history while archiving or restoring.
membership.getpersonIdMemberships and email eligibility.
membership.addManygroupId, personIds (1 to 20)Add visible people to a manual group with per-person results and safe retries.
membership.add / membership.removepersonId, groupId; reset? on addJoin or leave a group; a conditions group stores an include or exclude exception.
membership.movepersonId, fromGroupId, groupId, reset?Leave one manual group and join another.
membership.stagepersonId, groupId, stageIdSet a stage; null clears it without leaving the group.
membership.advancepersonId, groupIdMove to the next nonterminal stage.
membership.overridepersonId, groupId, overrideUse include, exclude, or null for automatic membership.
membership.holdpersonId, groupId, holdResume an earlier saved pause; creating a new pause is no longer supported.
rules.listarchived?List rules; archived:true includes archived rules.
rules.getidRead the saved rule and version.
rules.createname, trigger, conditions?, actions, enabled?Create a rule. Pass enabled:false to review it first.
rules.placeid, owner, expectedRevisionChange display placement only; preserve execution and reject stale saves.
rules.updateid, changed fieldsSave a new rule version.
rules.enable / rules.pause / rules.archiveidActivate future events, pause, or archive.
rules.validaterule fields or idValidate references and automation cycles without saving.
rules.previewrule fields or id; personId?Evaluate a person or return validation only; sends nothing.
rules.runsruleId?, cursor?, limit?Inspect saved outcomes and failed actions.
rules.retryrunId or ruleIdRetry eligible failed work; completed action receipts stay saved.
attachments.creategroupId, stageId?, emailKind, emailId, enabled?, name?, conditions?Create an ordinary group/stage entry rule for a template or series.
person.permissionpersonId, allowed; backfill?, expectedEmail?, evidence?, requestKey?; or reviewed people batch (1–25)Record a decision or repair missing permission without automation. Unsubscribe and suppression still win.
audience.previewaudiencePreview matched and eligible People for a campaign.
import.previewcsv, mapping?, headersOnly?Validate a CSV and match existing contacts; sends nothing.
import.startcsv, mapping?, groupIds?, requestKeyStart a saved import.
import.getidRead the import cursor, outcomes, and errors.
import.retryidRetry failed rows after correcting saved identity conflicts.

Limits, timing, and recovery

  • Worker timing: the existing People worker runs every five minutes and handles saved imports, account sync, condition groups, timed rules, and pending actions. Work starts on a later scheduled pass; a busy queue or retries can take longer than five minutes.
  • Temporary failures: eligible rule actions retry with bounded backoff, up to eight attempts. Permanent errors stay visible until corrected. Completed actions retain their receipts.
  • Safe activation: new UI rules and attachments start paused. Enabling applies to future events. Updating a rule changes its version and does not replay old events.
  • Safe membership: cycle checks reject rules that would repeatedly add/remove or move people in a loop. Stage guards prevent late events from resetting later progress.
  • Safe email: sending uses the existing monthly/hourly/site limits, platform cap, provider pacing, and kill switch. Permission and suppression are checked at delivery.
  • Safe retries: reuse a request key only for the same import or send. Inspect the saved job/run before starting a replacement.
  • Safe changes: rename labels while retaining IDs. Archive instead of deleting records. Review paused rules when restoring groups or stages.
Keep scope clear.

People does not replace the raw Inbox, Store orders, or the email send ledger. It combines the contact’s history and organization. Those source tools and their existing CLI commands remain available.

External customer sync

Keep integration code, credentials, scheduling, checkpoints and failures in your own service. Destination writes use the shared public CLI and site access. Source reads do not require a destination database write connection.

Look up an exact email or phone, including hidden matches. Fill blanks or update only values your integration last wrote. Supply expected values so concurrent owner edits fail without being overwritten. Historical updates, membership additions and notes skip automation; later meaningful changes can use normal events. New-contact imports need an explicit permission choice. Existing decisions and delivery restrictions stay saved.

Persist exact request inputs before making calls. Reuse stable note keys after a lost response; changed inputs conflict. A retry cannot recreate an owner-deleted note. Review identity conflicts and saved removals rather than overriding them. Your scheduler can use a configurable five-minute default, leases and bounded passes.

Exact identity lookup

frontpage people workspace person.lookup --site YOUR_SITE --data "{\"email\":\"alex@example.com\"}" --json

Conditional contact update

frontpage people workspace person.update --site YOUR_SITE --data "{\"personId\":\"PERSON_ID\",\"patch\":{\"phone\":\"+12025550100\"},\"expected\":{\"phone\":null},\"historical\":true}" --json

Retry-safe historical note

frontpage people workspace notes.add --site YOUR_SITE --data "{\"personId\":\"PERSON_ID\",\"body\":\"Verified billing context\",\"requestKey\":\"YOUR_STABLE_KEY\",\"historical\":true}" --json

Manual saved emails from People

Templates hold reusable content. Series hold ordered templates and delays. Neither definition saves an audience. In People, Email → Use a template creates an independent campaign draft with the current audience. Review content and recipients before sending. Editing the campaign never changes the source template. The template editor’s Create campaign shortcut uses saved content and then asks for a campaign audience.

Add to a series accepts selected person IDs or a full group, stage or filtered audience at explicit review. An Off series saves people waiting at step 1 without a send or timer. On starts their first email subject to sender setup, delivery permission, windows and limits. Later waits start after successful sends. Turning Off preserves progress and due dates; turning On resumes the next unsent step. Completed or cancelled people never restart. Review shows eligible/excluded/already-enrolled counts and remainingAllowance (null while Off). Enrollment saves immediately without another Save series. Campaign recipient-only edits save with the campaign, including explicit addresses and clears, and never grant consent or create CRM contacts.

Copy a saved template

frontpage people workspace template.campaign --site YOUR_SITE --data "{\"templateId\":\"TEMPLATE_ID\",\"requestKey\":\"YOUR_STABLE_KEY\",\"audience\":{\"personIds\":[\"PERSON_ID\"]}}" --json

Review a manual series start

frontpage people workspace series.preview --site YOUR_SITE --data "{\"seriesId\":\"SERIES_ID\",\"personIds\":[\"PERSON_ID\"]}" --json

Preview returns five-person batches. Copy each batch’s personIds and reviewToken into series.enrol with its seriesId and a unique stable requestKey. The service accepts at most 25 people per request. Reuse the original payload and key after a lost response. Results distinguish enrolled, already enrolled, skipped and failed. A fresh review uses new keys.

People supplies first-name and unsubscribe values. Edit copied campaign content or choose another series when templates need order, booking or other caller-only values. Never invent missing values. Never send or enrol without an explicit user instruction. Existing API, CLI, rules, self-tests, editor tools and delivery activity remain supported.

Customer invoices

Payments can select a visible CRM person with a billing email or create/match a person inline when sending. Preview does not create a person. Invoices and billing events link to that same person and do not grant marketing consent. See Payments.

Group and stage email tasks

Group details has an Email section. It shows ordinary rules whose actual trigger is someone joining that group and whose actions send a template or start a series. It uses the same records as Rules. Saved conditions and extra actions stay visible. Moving a rule's placement does not change where it runs.

Use the shared Add email control to select a saved template, create a template, or use a saved series. Stage Manage rules uses the same template task. Create the group first. Existing groups save name and color independently; membership edits stay inside How people join until Apply changes. Adding an email does not apply membership edits. Canceling email setup adds no attachment. Creating or attaching saves one paused rule with no extra conditions. The attached email notice explains that Activate turns it on for future people joining the group. Activate and Pause save immediately, without another group save. Nothing activates, enrolls or sends merely from attachment. The older attachments.create operation keeps its documented defaults; the completion below always creates paused rules.

The existing command and agent operation handle the complete save:

  • CLI: frontpage mail save --file save.json --site YOUR_SITE --json
  • Public API: POST /api/v1/sites/YOUR_SITE/mail/save
  • Agent: save_mail_editor

First read groups.list. Copy the group's emailVersion into attachment.expectedGroup. For a stage, also copy its id and emailVersion into stageId and expectedStage. These are saved configuration checks; changing membership counts does not invalidate the task. Never guess a version.

{
  "requestKey": "a-stable-group-template-save",
  "document": {
    "kind": "template",
    "name": "Group welcome",
    "subject": "Welcome, {{first_name}}",
    "preheader": "Thanks for joining us.",
    "blocks": [{"type":"paragraph","text":"Welcome to our group."}],
    "variables": []
  },
  "attachment": {
    "groupId": "GROUP_ID_FROM_GROUPS_LIST",
    "expectedGroup": "EXACT_EMAIL_VERSION_FROM_GROUPS_LIST"
  }
}

The response returns complete:true, document and rule. Repeating the same requestKey and payload returns the same pair. Changed payloads conflict. A failed transaction leaves neither a new template nor a new rule. Keep the exact request after a lost response and retry it before starting another save.

To attach an existing source, omit document. Add either attachment.templateId or attachment.seriesId and its expectedSourceUpdatedAt from a fresh template/series get. A series needs at least one saved step. The result includes the source and paused rule. To edit shared content from an existing entry rule, supply the existing document with its stable id, plus attachment.ruleId and expectedRevision from rules.get. This saves the content while leaving the rule's conditions, actions, version, enabled state, activation time and history unchanged. Stale group, stage, source or rule checks return a conflict; keep the draft and review the saved records before making a revised request.

The workspace uses the existing template editor and returns to the source group or Stages after Save email & attach to group. Unfinished group, rule, stage, picker and template edits stay in memory only. Closing the membership editor with X, Cancel, or Escape immediately discards its unapplied edits, without a confirmation. Other unfinished editors retain the shared unsaved-changes modal: Cancel keeps editing; Discard and continue loses those edits and leaves without a network request. Refresh or closing the tab discards unfinished work without a browser prompt. Clean navigation and opening saved records show no warning. There is no editor backup or Recover save action. A template task keeps its source context in memory during the active flow; a refreshed task must be reopened from its source. A self-test sends only test content and does not complete a pending group task. Retry a submitted save with the exact original key and payload after a lost response; this does not require an editor backup.

Templates remain shared. Changes affect future uses; already queued messages keep their content snapshots. Template get exposes ruleUsage; the UI also shows rule usage beside existing dependencies. Deleting a template used by a live rule, including a paused rule, fails with a conflict naming the rule. Remove the template action or archive the rule first. Series, form and receipt dependencies still apply. Archiving a rule keeps its run history and saved actions. Never activate, enroll or send without the user's instruction.

Full audience series review and person activity

series.preview accepts either the existing personIds array (up to 1,000 IDs) or an audience object, never both. Audience uses the existing People matcher: personIds, groupIds, stageIds, and supported filters. ID lists combine as a union; filters narrow the result. All people uses {"filters":{}}. Series audiences cannot contain addresses. Group/stage IDs must belong to this site and remain available.

The service resolves the whole audience at explicit review, up to 10,000 people, without using the list's page or sort. It returns the existing counts, eligiblePeople, excluded reasons and reviewed batches. An empty matched audience returns zero counts and no batches. The UI keeps the eligible total and omits exclusion details; CLI/agent responses retain reasons.

frontpage people workspace series.preview --site YOUR_SITE --data '{"seriesId":"SERIES_ID","audience":{"groupIds":["GROUP_ID"],"filters":{"q":"Alex"}}}' --json
frontpage people workspace person.activity --site YOUR_SITE --data '{"personId":"PERSON_ID","limit":50}' --json

Copy each returned batch's exact personIds and reviewToken into series.enrol, with the series ID and a unique stable requestKey for that batch. Keep completed batches and retry uncertain batches with the original payload and key. Reusing a key with other IDs conflicts. Existing and completed/cancelled enrollments never restart. Off waits at step 1 without sending; On uses the existing sender, permission, window and allowance checks. This is a one-time selection captured at review: later membership changes do not add or cancel manual enrollments. Use an explicit group-entry rule for later entrants. Never enroll or send without the user's instruction.

person.activity is a read-only People workspace operation shared by UI, CLI, public API and agent. It returns events, total, and nextCursor; limit defaults to 50 and accepts 1–100. Pass the same personId and returned cursor to load older entries. Cursors keep timestamp precision and tie-break by stable entry identity; a cursor from another person/site is invalid. Queue time fixes email ordering, so delivery updates do not move it into another page. Totals are current reads and can change as activity arrives. UI tab counts refer to loaded entries.

The feed combines CRM history and mail ledger rows. Mail entries use id: "mail:SEND_ID", type: "email.send", and payload containing sendId, subject, recipient snapshot, kind, test, status, error and status update time. Read frontpage mail sends get --id SEND_ID for saved content and source details. Workspace links open that same saved send even when the source was deleted. sent means provider accepted; only delivered means a saved delivery confirmation. Drafts and waiting enrollments are not sends.

A saved send snapshot's crmPersonId supplies identity; a missing ID may use its saved same-site series origin. The read never guesses from today's email address and never backfills legacy records. It shows an explicitly linked CRM email event only once through the ledger. New campaign recipient snapshots retain the approved person ID, and queueing skips a recipient whose address moved to another person. Existing queued content, permission checks, group-removal rules and suppression behavior stay in place.

Groups and stages cutover

Groups and optional stages are the only membership/progress model. The global status/tag API routes, tool names and CLI flags are retired. Use --group/--stage, membership.add/remove and membership.stage. Exports include group and stage labels; they omit global status and tags. Saved historical events still display their original facts.

Opening overview, list, summary, activity or export does not initialize records. Authorized first creation or ingestion sets up a new site's normal progress group and stages. Existing default group IDs, assignments, permissions, paused work and rule receipts remain saved.

MCP access requires separate consent

Source-file access does not grant contact access. Request sites:read crm:read for People/email reads, or sites:read crm:read crm:write for management. Choose People and email access on the consent screen. It defaults to No People or email access. Existing connections do not gain these scopes. Management can send email or activate automation on the user's instruction; site role, plan, permission and delivery checks still apply.

Every client uses the same 102 named People/email tools. Select the action by tool name, not an operation field. For example:

  • Groups: frontpage_people_groups_create, frontpage_people_groups_get, frontpage_people_groups_archive.
  • Contacts: frontpage_people_contacts_list, frontpage_people_contacts_get, frontpage_people_contacts_export.
  • Drafts: frontpage_save_mail_editor, frontpage_get_mail_email, frontpage_delete_mail_email.
  • Campaign review and confirmation: frontpage_mail_campaign_preview, frontpage_mail_campaign_send.

Named calls take slug and the action's payload; read their current strict schemas. The verified connection supplies site authority and actor. Native roles, eligibility, permission and delivery checks remain. The former generic executors and guide tools have no aliases. Use https://frontpage.host/developers/mcp for MCP setup and the current named contracts.

Reads accept resultOffset (default 0) and resultLimit (default 50, maximum 100) for top-level arrays. Continue with resultPage.nextOffset and the same named tool/payload. resultPage.totals records full array lengths. Native list/activity cursors belong in payload. Retain exact audience batches and review tokens; re-review after stale-token conflicts.

Writes are not universally idempotent. Keep native request keys, revisions, review tokens and receipts. After an unknown outcome, read back before creating again. Never send, enroll or activate without the user's instruction.

For CSV exports, use frontpage_people_contacts_export with textOffset from textPage.nextOffset and expectedTextVersion from textPage.version. A changed export conflicts instead of combining snapshots. textLength defaults to 4,000 UTF-16 units and accepts up to 8,000.

Saved template history

Open, edit and save old template copies through the same Email tools. Template saves keep both simultaneous edits; the last save becomes current. Group, rule and series checks remain. See the Email template history guide for CLI, API and agent commands.

Campaign scheduling

Campaigns can also schedule a future send through mail save with document.scheduledAt. A date on a draft never sends it. After reviewing content, audience and time, mail campaign send confirms the saved schedule; the existing worker waits until due. Send now uses scheduledAt:null. campaign get returns stored status and scheduledAt; pending plus a future time displays Scheduled in the UI. Groups and filters resolve when confirmed. Read MAIL.md, Campaign scheduling for complete save, review, retry, agent and MCP examples. Direct mail send email is for immediate unscheduled sends and cannot bypass a saved schedule.

For the owner agent, use save_mail_editor with a one_off document and scheduleLocal:{dateTime:"2030-11-15T09:00",timeZone:"America/Chicago"}. The server converts local time to UTC. Use the owner's timezone; ask if unknown. Future ISO scheduledAt including Z or an explicit offset remains supported; do not supply both. Omission preserves timing; null clears it. Read get_mail_email after saving and manage_people with area:campaign, operation:get for saved timing/audience, then operation:preview for the exact audience and eligible count. Show the owner the content, recipients and local send time. On explicit confirmation, call manage_people with area:campaign, operation:send, payload:{documentId,requestKey,review:{audience,eligible,documentUpdatedAt,scheduledAt}}. Use the post-save document timestamp, preview count and canonical saved UTC time. These values must still match; a changed review needs a fresh read and approval. Keep the original requestKey and payload after an uncertain result and read back state before retrying. Never use direct send_mail_email to bypass timing. MCP uses frontpage_save_mail_editor, frontpage_mail_campaign_send, frontpage_mail_campaign_get and frontpage_mail_campaign_preview, with the same native payloads and review checks. Before queueing, campaign.unschedule cancels the scheduled email to Draft; campaign.reschedule changes only time. Both use documentUpdatedAt from a fresh campaign.get and a stable requestKey. Reschedule takes scheduleLocal or scheduledAt. Get approval for the new time and retain confirmed recipients/content. Cancellation keeps the email/audience for editing; scheduling again needs fresh confirmation and a new send key. Old accepted retries cannot undo later actions. UI, CLI, agent and MCP share this service. See the Mail guide for examples.