Chase Engine

Overview

The chase engine automatically sends reminder emails to clients who have not completed their intake. You configure the cadence when you call define_intake; BriefGate handles timing, deduplication, and quiet hours from that point on. The agent never has to babysit email threads or remember to follow up.

When a client submits all required items, the chase engine stops automatically.

Schedules

Schedule Reminders Cadence
gentle T+3d, T+8d, then every 14 days For sensitive client relationships
default T+2d, T+5d, T+9d, then weekly Recommended for most projects
aggressive T+1d, T+3d, T+5d, then every other day For hard deadlines
custom Every chase_interval chase_interval_unit (default every 3 days) When no preset fits
off None Manual only

Set the schedule when creating the intake:

json
{
  "project_name": "Bella Cucina Website",
  "client": {"email": "owner@bellacucina.cz"},
  "items": [...],
  "chase_schedule": "default"
}

The cadence can be changed after the intake is sent — see Changing the schedule later.

Solo, Studio, and Agency plans have access to all schedules. Free plan has access to default and off only.

Custom interval

When none of the presets fits, set chase_schedule to custom and give the interval as a number plus a unit:

json
{
  "project_name": "Bella Cucina Website",
  "client": {"email": "owner@bellacucina.cz"},
  "items": [...],
  "chase_schedule": "custom",
  "chase_interval": 5,
  "chase_interval_unit": "minutes"
}

chase_interval_unit is one of minutes, hours, days, and defaults to days. The first reminder lands one interval after the invite and one every interval after that — 5 days gives T+5d, T+10d, T+15d, and so on.

Bounds: at least 5 minutes, at most 90 days. Anything shorter than five minutes runs into mail-provider rate limits and reads as spam to the recipient, so the API rejects it.

Both fields are optional. chase_schedule: "custom" on its own reminds every 3 days. Sending either alongside a preset schedule is rejected with a 422, because the presets have fixed cadences and silently ignoring the field would leave you believing you had changed one.

Unlike gentle, a custom cadence does not skip weekends.

A fixed time of day

A cadence measured in whole days can be pinned to a local time instead of drifting with whenever the invite happened to go out:

json
{
  "chase_schedule": "custom",
  "chase_interval": 1,
  "chase_interval_unit": "days",
  "chase_at_time": "07:00"
}

That reminds the client every morning at 07:00 in client.timezone, DST included.

Reminder cap

An intake stops chasing after max_reminders reminders (default 3), marks itself stalled, and fires the intake.stalled webhook so you can step in. A rapid cadence would burn through three attempts in minutes, so raise the cap when you shorten the interval:

json
{
  "chase_schedule": "custom",
  "chase_interval": 30,
  "chase_interval_unit": "minutes",
  "max_reminders": 12
}

The maximum is 1000. Passing "max_reminders": "unlimited" removes the cap entirely — worth it only for a genuinely open-ended chase, because an intake ignored a hundred times is unlikely to be rescued by another email and the sending domain's reputation pays for every one of them.

Changing the schedule later

PATCH /v1/intakes/:id can change chase_schedule, chase_interval, chase_interval_unit, chase_at_time, max_reminders and respect_quiet_hours after the intake has already gone out — no need to delete it and start over, which would re-send the invitation and hand the client a second link.

json
{
  "chase_schedule": "custom",
  "chase_interval": 6,
  "chase_interval_unit": "hours",
  "max_reminders": 12
}

Changing any of those fields — or client.timezone or due_date, both of which the cadence math reads — cancels every reminder still scheduled and re-plans from the new settings. Reminders already sent are not undone and still count toward max_reminders: lowering the cap after three of five reminders have gone out leaves two attempts, not five.

If the intake has already stalled (see Reminder cap above), changing the cadence alone does not resume chasing — a stalled intake stays stalled until max_reminders is raised past the number already sent, or set to "unlimited". That is also the escape hatch: an intake that stalled with max_reminders: 3 picks up automatic reminders again the moment you PATCH it to 5 or "unlimited".

Quiet hours

The chase engine respects client working hours to avoid sending reminders at inconvenient times.

If a scheduled send falls in a quiet window, it is held until the next allowed time — it is not skipped.

What the reminder email contains

Each reminder is tailored to what is still missing:

The email is written from your perspective, not BriefGate's. Clients see a professional reminder from you, not from a third-party tool.

On the shared sending domain, the From name still reads "Your Name via BriefGate" — that's true on every plan, including paid ones. The only way to remove "BriefGate" from the From line entirely is the Studio or Agency plan with a verified custom sending domain (below).

Custom sending domain (Studio and Agency)

On the Studio or Agency plan, configure your own sending domain (e.g. intake@yourcompany.com) so clients see your domain in the From field and your name alone, with no "via BriefGate". The account settings page shows the DNS records to add and checks them automatically. See Custom Domains for the full setup guide.

Until a custom domain is configured, emails are sent from intake@briefgate.dev, with your name in front of it ("Your Name via BriefGate").

Manual chase

Send a reminder immediately, outside the schedule:

Via MCP:

send_chase(intake_id: "int_01J3K...", channel: "email")

Via REST:

POST /v1/intakes/:id/chase
json
{
  "channel": "email"
}

channel defaults to email; sms is also accepted on plans with SMS enabled.

This does not reset the schedule — scheduled reminders continue as planned after the manual one. It does, however, count toward max_reminders: a manual send is one of the reminders that can trip the reminder cap and stall the intake, same as a scheduled one.

Manual chase is rate-limited: at most 1 per intake per hour, and 20 per account per hour.

Escalation flow

When email delivery consistently fails, the chase engine signals the agent:

  1. Client email hard-bounces (or keeps bouncing temporarily with no delivery) → chase.bounced webhook fires, BriefGate stops sending to that address
  2. The intake runs out its reminder allowance without being completed → intake.stalled webhook fires with the missing_items list
  3. Agent receives intake.stalled and can escalate by other means (notify a human, update project status, etc.)

The missing_items array in intake.stalled tells you exactly which items are still outstanding, so you can relay that to whoever handles escalation.

More than one person at the client

Some material belongs to a company rather than to a person, and either of two directors could send it. An intake can be addressed to up to five people who share one portal link:

json
{
  "client": {
    "email": "first@bellacucina.cz",
    "name": "Marek",
    "also_notify": [{ "email": "second@bellacucina.cz", "name": "Jana" }]
  }
}

Everyone on the list gets the invitation, every reminder, and the same link. Whoever opens it first can supply everything; the others see what is already done rather than being asked for it again.

Each of them gets their own message. Nobody's address appears in anyone else's copy, each is greeted by their own name, and each carries its own bounce state — so one dead address stops mail to that person and to nobody else.

Add or remove someone later with POST /v1/intakes/:id/recipients and DELETE /v1/intakes/:id/recipients/:email. Adding sends nothing by itself: the new person is included from the next reminder onward, or you can send one now with send_chase. The primary client cannot be removed this way — it is the address the intake was created for.

Bounce handling

Not every bounce means the address is dead, so BriefGate reads what the mail provider reported before it acts:

What Resend reported What BriefGate does
Spam complaint Retires the address, stops chasing it
Permanent bounce (mailbox does not exist, domain rejects) Retires the address, stops chasing it
Bounce after a confirmed delivery of the same message Keeps the address, schedule untouched
Temporary bounce (mailbox full, greylisting) with no delivery Keeps the address; the third undelivered one in a row retires it

The "bounce after delivery" case is common and easy to misread: the client's server accepts the message, then a forwarding rule or a quarantine gateway behind it sends a delivery failure seconds later. The person has the email in their inbox. Treating that as a dead address would silently cancel every reminder on the intake.

Whenever an address is retired, BriefGate:

  1. Fires the chase.bounced webhook
  2. Marks that email address as bounced on this intake
  3. Stops all future chase emails to that address (to protect your sender reputation)

Only that address stops. On an intake addressed to several people, the others go on being reminded — the webhook payload carries recipient and still_chasing so you can tell which of the two happened. The webhook also fires for a bounce that did not retire the address (still_chasing: true, plus bounce_type and delivered_before), so you can see a flaky mailbox before it becomes a dead one.

A bounced primary address cannot be corrected on an existing intake — there is no endpoint for changing client.email after creation. Either add a working address with POST /v1/intakes/:id/recipients, or create a new intake for the items they still owe you and let the old one be archived.

Language

Every email is written in the client's language, chosen in this order:

  1. client.language on the intake
  2. default_language on the account (PATCH /v1/account)
  3. English

Supported: cs, sk, pl, de, es, en. The subject, the body, the button and the date format all follow it — including plural agreement, which differs between languages that look similar. Polish treats 22 as "few" and 25 as "many"; Czech treats both as "many".

Set default_language once if you work in one market. Without it, an intake that forgets client.language sends that client an English email and nobody finds out.

Writing your own subject and intro

email_copy replaces the built-in lines, on the account (as a default) or on a single intake (overriding it, field by field). invite_subject/invite_intro customize the first invitation email; reminder_subject/reminder_intro customize the chase emails themselves — set the reminder pair if it's the reminders you want to change:

json
{
  "email_copy": {
    "invite_subject": "{sender} needs {count} things for {project}",
    "invite_intro": "Hi {client}, for {project} we still need a few things — it takes about {minutes} minutes.",
    "reminder_subject": "Reminder: {sender} still needs {count} things for {project}",
    "reminder_intro": "Hi {client}, just a nudge — {project} is still waiting on a few things ({minutes} minutes or so)."
  }
}

Placeholders: {sender}, {project}, {client}, {count}, {minutes}, {due}. Anything else is rejected with a 422 rather than rendered literally — a client reading {proejct} is a mistake nobody notices until after they have read it.

The layout, the button and the footer stay as they are. Values are escaped where the line lands inside HTML, so a project name is never able to inject markup.