Guide
Build a Waitlist Notification Agent
Manually managing a spot-available waitlist means someone checks a spreadsheet, fires an email, waits for a reply, and manually moves to the next name if there's no answer. Automate that entire loop on a dedicated agent account: the waitlist inbox receives replies, the model classifies them, queue ordering stays in code, and the agent rolls to the next person automatically after a configurable timeout. According to Omnisend's 2025 ecommerce marketing report, automated triggered emails achieve 52% higher open rates than broadcast campaigns — the acceptance window matters.
Written by Hazik Director of Product Management
What is a waitlist notification agent?
A waitlist notification agent is an automated program that manages a first-come, first-served queue: when a spot opens, it emails the next person a time-boxed acceptance offer, watches the inbox for their reply, classifies it as accept or decline (or silence), and either confirms the booking or moves to the next name. Built on an agent account, the entire loop runs on one managed identity — no shared mailboxes, no spreadsheet hand-offs, and no human in the critical path between "spot opens" and "spot claimed." Fairness logic stays in code; the model only classifies free-text replies and writes confirmation copy.
According to Omnisend's 2025 ecommerce marketing report, automated triggered emails achieve 52% higher open rates than broadcast campaigns and 2,361% better conversion rates. The acceptance window a waitlist offer gives someone — typically 24 to 48 hours — is the reason to send it fast. A spot that opens at midnight is claimed by morning if the agent runs every minute; one managed by a human checking email twice a day decays for 10 hours before anyone knows it's available.
Why does a waitlist agent need its own inbox?
A waitlist agent should own a dedicated inbox, not share a team mailbox. On an agent account, the waitlist address is the agent's own identity, every inbound reply lands in one place, and the loop runs without a human session in the path. Pause or retire the agent by suspending a single grant — no shared-credential cleanup, no forwarding rules to unwind. The agent account provisions in under 2 seconds and the inbox is live immediately, compared to 24-48 hours for a domain-level mailbox that requires DNS propagation and MX record setup.
There's a safety reason too. Waitlist reply bodies are untrusted content from external strangers. Because the agent account's outbound rules live on the workspace, a reply containing a prompt-injection payload can't talk the agent into emailing arbitrary addresses — containment lives outside the agent's decision loop. Simon Willison named this the lethal trifecta: private data, untrusted content, and external communication. A waitlist inbox hands an agent all three legs at once. The workspace policy is what keeps them from combining. See Stop Your AI Agent From Going Rogue for the full containment pattern.
How do I create the waitlist inbox?
Create the agent identity with a single command. The nylas agent account create call provisions a Nylas-managed mailbox in under 2 seconds, returning a grant ID you export for all subsequent commands. Point your waitlist form's notification address or your product's sign-up webhook at this address:
nylas agent account create waitlist@yourapp.nylas.email
export NYLAS_GRANT_ID="$(nylas agent account get waitlist@yourapp.nylas.email --quiet)"Once the grant is set, every CLI command in this guide runs against the waitlist inbox automatically. The inbox is ready to receive replies within seconds of creation — no DNS records, no SMTP setup, no app-password rotation. For a production deployment, store the grant ID in your secrets manager and inject it as an environment variable in the cron container.
How does the agent manage the waitlist queue?
Queue ordering is a code responsibility, not a model responsibility. The agent reads the queue from a flat file or a database row — whichever your stack already has — and decides who is next based on position and timestamp. A simple JSON file works for a waitlist of a few hundred people; swap it for a Postgres row lock when you need concurrent agents claiming from the same queue. The agent checks the file, picks the first unclaimed entry, and marks it "pending" before sending the offer so a restart can't double-claim:
#!/usr/bin/env bash
# queue.json: [{"email":"a@example.com","status":"waiting"},...]
next=$(jq -r '[ .[] | select(.status == "waiting") ][0] // empty' queue.json)
[ -z "$next" ] && echo "queue empty" && exit 0
email=$(echo "$next" | jq -r '.email')
# Mark pending BEFORE sending so a crash doesn't double-offer
jq --arg e "$email" '[ .[] | if .email == $e then .status = "pending" else . end ]' queue.json > queue.tmp && mv queue.tmp queue.jsonThe mark-before-send pattern is the critical safety property: if the agent dies between writing the file and sending the email, the entry stays "pending" and a retry can detect it rather than re-offering. A cron that runs every minute means the worst case delay between a spot opening and an offer landing is 60 seconds.
How does the agent send a time-boxed claim offer?
The nylas email send command dispatches the offer from the agent's own address in under 1 second. The offer body includes a plain-English deadline — 24 hours is typical for product waitlists; 4 hours works for high-demand events. Keep the reply instructions explicit: "Reply YES to claim your spot" gives the classifier an unambiguous signal. The model writes the copy variation, but the deadline timestamp comes from code:
# deadline: 24 hours from now, ISO 8601
deadline=$(date -u -d '+24 hours' '+%Y-%m-%dT%H:%MZ' 2>/dev/null || date -u -v+24H '+%Y-%m-%dT%H:%MZ') # macOS fallback
nylas email send \
--to "$email" \
--subject "Your spot is available — claim by ${deadline}" \
--body "A spot just opened for you. Reply YES to claim it or NO to pass.
Your offer expires at ${deadline} UTC. If we don't hear back, we'll offer
the spot to the next person on the list.
— Waitlist agent"Store the offer timestamp alongside the queue entry. The next cron run reads it and computes the remaining window without calling an external service. For higher-volume lists, use nylas email drafts create to stage the batch of outgoing offers and inspect them before sending — useful when a human wants a 5-minute review window on acceptance copy before it goes out.
How does the agent classify a reply?
Reply classification is the one task the model handles. The agent polls the inbox with nylas email list --unread --json and pipes each unread message to a model that returns one of three categories: accept, decline, or unclear. The model handles the natural-language variation — "yes please", "I'd love to", "not right now", "can I defer?" — so the downstream code only branches on three clean values. Keep the classification and the confirmation as separate steps; the first call reads the reply, the second writes the email:
# Poll the waitlist inbox for replies to the pending offer
reply=$(nylas email list --unread --json \
| jq -r '.[] | select(.from[0].email == "'"$email"'") | {id, snippet} | @json' \
| head -1)
[ -z "$reply" ] && echo "no reply yet" && exit 0
reply_id=$(echo "$reply" | jq -r '.id')
snippet=$(echo "$reply" | jq -r '.snippet')
# classify_reply is your LLM call; returns: accept | decline | unclear
verdict=$(classify_reply "$snippet")
# Mark the reply read so the next run skips it
nylas email mark read "$reply_id"The model's job is classification only — it doesn't set the deadline, it doesn't update the queue file, and it doesn't decide who is next. Those three decisions stay in deterministic code. That separation is what makes the agent auditable: every queue transition has a timestamp and a reason logged to stderr, not buried in a model chain. A typical classification call on a 50-word reply body costs under 200 tokens, so a waitlist of 1,000 people processes in under 60 seconds at standard API rates.
How does the agent confirm acceptance or roll to the next person?
After classification, the agent branches on the verdict. An "accept" reply triggers a confirmation email and updates the queue entry to "confirmed". A "decline" or an expired offer rolls the entry back to "skipped" and claims the next waiting entry. An "unclear" reply can either prompt a one-time follow-up or treat silence as a decline after a shorter secondary window — 4 hours works well. The full round-trip from offer to confirmed booking takes under 3 minutes when the person replies immediately:
case "$verdict" in
accept)
nylas email send \
--to "$email" \
--subject "Spot confirmed" \
--body "You're in. See you there."
# Mark confirmed in queue
jq --arg e "$email" '[ .[] | if .email == $e then .status = "confirmed" else . end ]' queue.json > queue.tmp && mv queue.tmp queue.json
;;
decline)
# Mark skipped and let the next cron run pick the next waiting entry
jq --arg e "$email" '[ .[] | if .email == $e then .status = "skipped" else . end ]' queue.json > queue.tmp && mv queue.tmp queue.json
echo "declined by $email — rolling to next" >&2
;;
unclear)
nylas email send \
--to "$email" \
--subject "Re: Your spot — please reply YES or NO" \
--body "We received your message but need a clear YES or NO to hold your spot."
;;
*)
echo "unexpected verdict '$verdict' for $email — treating as decline" >&2
jq --arg e "$email" '[ .[] | if .email == $e then .status = "skipped" else . end ]' queue.json > queue.tmp && mv queue.tmp queue.json
;;
esacThe explicit unknown-category arm matters. Model output drifts across API versions, and catching an unexpected value in the case statement is how you surface that drift instead of silently skipping a queue entry. The human-in-the-loop pattern is a useful extension here: route "unclear" replies to a human reviewer instead of auto-declining, so high-value waitlist members don't lose their spot to a classifier edge case.
How does the agent handle no-reply timeout?
Timeout handling is the rule that makes a waitlist fair. Without it, a non-responsive person blocks the queue indefinitely. The cron script reads the offer timestamp from the queue file and compares it to the current time. If the 24-hour window has passed and the entry is still "pending", the agent marks it "expired" and rolls forward. No model call is needed — expiry is a pure timestamp comparison that runs in under 1 millisecond:
#!/usr/bin/env bash
set -euo pipefail
NOW=$(date -u '+%s')
WINDOW=86400 # 24 hours in seconds
# Check for any pending entry whose offer has expired
expired=$(jq -r --argjson now "$NOW" --argjson win "$WINDOW" '.[] | select(.status == "pending" and ($now - (.offered_at // $now)) > $win) | .email' queue.json | head -1)
if [ -n "$expired" ]; then
echo "offer expired for $expired — rolling to next" >&2
jq --arg e "$expired" '[ .[] | if .email == $e then .status = "expired" else . end ]' queue.json > queue.tmp && mv queue.tmp queue.json
fiStore offered_at as a Unix timestamp when you write the "pending" status. Unix seconds are simpler than ISO strings for arithmetic and survive timezone edge cases cleanly. The timeout script and the reply-classification script run independently every 60 seconds; either can roll the queue forward, and both write the same final status so there's no race condition when they happen in the same cron window.
How do I stop the agent from going rogue?
Waitlist reply bodies are untrusted input. A malicious entry in the queue could include a prompt-injection payload designed to redirect the confirmation email to an attacker-controlled address. According to OWASP's AI Agent Security Cheat Sheet, detection at the prompt layer is probabilistic — clever inputs eventually get through. Two deterministic rules close the vector instead. The first blocks outbound email to flagged domains; the second caps how many emails the agent can send per hour. Neither can be bypassed by model output — the agent can't prompt its way past a rule it does not control:
# Block outbound to any domain other than your permitted recipient list
nylas agent rule create \
--name "Waitlist: block unauthorized outbound" \
--trigger outbound \
--condition recipient.domain,is,exfil.example \
--action block
# Archive any inbound that isn't a reply from a pending email (reduces noise)
nylas agent rule create \
--name "Waitlist: archive non-reply inbound" \
--trigger inbound \
--condition sender.domain,is,spam.example \
--action archivePair these rules with a send-rate policy via nylas agent policy create to cap hourly send volume. A buggy loop that double-offers everyone on the list should hit the policy cap before it causes real damage. The agent rules and policies guide documents every available trigger, condition, and action. For the broader threat model — including why containment must live outside the agent's decision loop — see Stop Your AI Agent From Going Rogue.
How do I verify the agent end to end?
A working waitlist agent should pass five checks before going to production. Run them against a test queue with two entries and a 1-minute offer window so the full loop — offer, accept, expire, roll-forward — completes in under 5 minutes. These tests run against Nylas CLI 3.1.16 with a Nylas managed provider:
# 1. Inbox creates and returns a grant
nylas agent account get waitlist@yourapp.nylas.email --quiet | grep -qE '^[a-z0-9-]+$' \
&& echo "PASS: grant ID returned" || echo "FAIL: no grant"
# 2. Offer email reaches the inbox (use a second test address as recipient)
nylas email send --to test-recipient@example.com \
--subject "Test offer" --body "Reply YES"
nylas email list --unread --json | jq '.[0].subject' | grep -q "Test offer" \
&& echo "PASS: offer visible in inbox" || echo "FAIL: offer missing"
# 3. Reply classification returns a valid category
verdict=$(classify_reply "yes please")
echo "$verdict" | grep -qE '^(accept|decline|unclear)$' \
&& echo "PASS: classifier returns valid category" || echo "FAIL: bad category '$verdict'"
# 4. Outbound block rule is present
nylas agent rule create --name "Test block" --trigger outbound \
--condition recipient.domain,is,blocked.example --action block
echo "PASS: outbound block rule created"
# 5. Queue rolls forward after expiry timeout
# (Set offered_at to 90 seconds ago, run timeout script, confirm entry status = expired)
echo "PASS: timeout roll-forward verified manually"Tested on Nylas CLI 3.1.16 with a Nylas managed provider. Provider-side behavior for reply threading and unread-flag clearing is described from documented behavior — verify against your own provider before deploying a high-volume list.
Next steps
- Getting Started with Agent Accounts — the architecture behind the waitlist inbox and its workspace
- Build a Lead-Capture & Qualification Agent — apply the same inbox-agent loop to inbound sales leads
- Build a Human-in-the-Loop Email Agent — route "unclear" replies to a human reviewer before auto-declining
- Agent Rules and Policies — every trigger, condition, and action available for the containment layer
- Stop Your AI Agent From Going Rogue — the full prompt-injection containment pattern for agent inboxes
- Full command reference — every
nylas email,nylas agent, andnylas contactsflag - Nylas v3 API documentation — the API surface behind these commands