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:
{
"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:
{
"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:
{
"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.
- The value is a 24-hour local time,
HH:MM. - It needs an interval measured in whole days. A sub-daily cadence cannot also land at one fixed time, so the API rejects the combination with a 422.
- It outranks quiet hours. Naming 07:00 means 07:00 — the reminder is not pushed to 08:00 just because the default quiet window opens then.
- Like every send, it carries a few seconds of jitter so a fleet of intakes does not hit the mail provider on the same second.
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:
{
"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.
{
"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.
- Reminders are held if the client's local time is outside 8:00–19:00.
- Set
respect_quiet_hours: falseon the intake to send around the clock. Worth doing for a deliberately rapid cadence, which would otherwise sit idle overnight. - With quiet hours on, a sub-daily cadence counts only the time the window is open: a 30-minute interval running out at 18:50 resumes at 08:05 the next morning rather than dropping every overdue reminder onto 08:00 at once.
- Client timezone is set via
client.timezoneindefine_intake(e.g."Europe/Prague"). Defaults to UTC if not set. - The gentle schedule never sends on weekends.
- Every other schedule, custom cadences included, sends on weekends too.
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:
- Only the items the client has not yet submitted (not already-submitted ones)
- A progress summary ("3 of 8 items remaining, approximately 6 minutes")
- A direct link to the portal (the intake's one stable magic link, its validity extended by this send — no login required)
- Your name and branding — on paid plans, the "Powered by BriefGate" footer is removed
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{
"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:
- Client email hard-bounces (or keeps bouncing temporarily with no delivery) →
chase.bouncedwebhook fires, BriefGate stops sending to that address - The intake runs out its reminder allowance without being completed →
intake.stalledwebhook fires with themissing_itemslist - Agent receives
intake.stalledand 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:
{
"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:
- Fires the
chase.bouncedwebhook - Marks that email address as bounced on this intake
- 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:
client.languageon the intakedefault_languageon the account (PATCH /v1/account)- 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:
{
"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.