Venue Agent — Full Blueprint

Claude-powered follow-up agent replacing the Make.com venue scenarios.
Version 0.1.0 · deployed to Cloudflare Workers · last updated 2026-04-19

1System Overview

Every day at 12pm Sydney, a Cloudflare Worker wakes up, asks Claude Sonnet 4.5 to "run the daily follow-up cycle," and hands it 6 tools for reading/writing Gmail, Sheets, and Slack. Claude decides per-venue whether to resolve (reply found), draft a follow-up (cycle 1 or 2), or escalate (cycle 3). All email drafts land in Alec's drafts folder — nothing auto-sends until you flip a flag.

Triggers
2
cron + manual HTTP
Tools exposed
6
get, check, read, draft, update, slack
External services
4
Claude, Gmail, Sheets, Slack
Cost / venue
~$0.016
USD, Sonnet 4.5 + cache
Monthly cost
~$0.47
at 5 venues/day
Runtime
~35s
5 venues · 14 tool calls

2End-to-End Flow

Cron
daily 01:00 UTC
🧠
Worker
src/index.ts
🤖
Claude Agent
Sonnet 4.5
🛠️
6 Tools
Gmail·Sheets·Slack
📬
Drafts + Updates
no auto-send

Two control paths into the Worker:

3File Layout

FileRoleLines
wrangler.tomlWorker config: cron schedule, env vars (SHEET_ID, RUN_MODE, model), secret placeholders~30
package.jsonDeclares @anthropic-ai/sdk + wrangler + typescript~25
tsconfig.jsonStrict TS + Cloudflare Workers types~15
src/index.tsEntrypoint. scheduled handler for cron, fetch handler for manual /run + /health~75
src/agent.tsClaude tool-use loop. Builds messages, parses tool_use blocks, dispatches, appends tool_result, loops to end_turn. 30-turn safety cap.~110
src/tools.ts6 tool schemas + runtime handlers + dispatcher~170
src/google.tsOAuth refresh-token → access token. Gmail thread/draft ops. Sheets batch read/update. RFC2822 builder with UTF-8 base64 body encoding.~160
src/slack.tsTiny webhook POST helper~10
src/prompts.tsSystem prompt + Day 1/2/3 email templates. Edit here to change tone.~60
DEPLOY.mdDeploy runbook (wrangler login → secrets → deploy)~80
scripts/seed-test-data.pyInserts 5 fake venue threads + Sheet rows for testing every branch~200

4The Agent Loop

Each run is a conversation with Claude. The loop in agent.ts:

// Pseudocode
messages = ["Today is 2026-04-19. Run the daily venue follow-up cycle now. [+ templates]"]
for turn in 0..30:
    response = claude.messages.create({ system, tools, messages })
    if response.stop_reason == "end_turn":
        break
    for block in response.content:
        if block.type == "tool_use":
            result = runToolWithRetry(block.name, block.input)
            messages.append(tool_result)
    messages.append(claude_response)

Prompt caching

The system block carries cache_control: { type: "ephemeral" }. First run writes the cache (~1.6K tokens). Subsequent turns within the same run read from cache (~8K tokens cache-read). Cost savings: ~40% on an average run.

Transient-error retry

Every tool call runs through runToolWithRetry. If the error message matches /\b(429|500|502|503|504|ECONNRESET|ETIMEDOUT)\b/, it sleeps 500ms and retries once. Hard errors (404, auth failures) are passed straight back to Claude as is_error:true — Claude then decides whether to skip the venue and include it in the Slack summary.

5The 6 Tools

📖
get_active_venues
Sheets read
💬
check_thread_replies
Gmail threads.get
📄
read_email_content
Gmail messages.get
✉️
create_followup_draft
Gmail drafts.create
📝
update_venue_row
Sheets batchUpdate
🔔
send_slack_alert
Webhook POST
ToolInputOutputSide effects
get_active_venuesnoneArray of Venue objectsread-only
check_thread_repliesthread_id{ messages: [{from, date, snippet}] }read-only
read_email_contentmessage_id{ from, subject, body }read-only
create_followup_draftthread_id, to, subject, body, in_reply_to?{ draftId }writes to Gmail Drafts
update_venue_rowrow_number, updates{status, cycle_day, last_action_date, resolved_date}{ ok: true }writes to Sheet
send_slack_alerttext{ ok: true }posts to #make_team

6Per-Venue Decision Tree

for each venue in get_active_venues():
thread = check_thread_replies(venue.thread_id)
any_reply = any(msg.from ∉ {alec@, lauren@, travel@easyweddings.com.au})

if any_reply:
update_venue_row(status="resolved", resolved_date=today)

elif venue.cycle_day == 1:
create_followup_draft(body=Day1Template.fill(venue))
update_venue_row(cycle_day=2, last_action_date=today)

elif venue.cycle_day == 2:
create_followup_draft(body=Day2Template.fill(venue))
update_venue_row(cycle_day=3, last_action_date=today)

elif venue.cycle_day == 3:
update_venue_row(status="escalated")
→ add to escalations list (Slack summary at end)

finally:
send_slack_alert(summary: X checked / Y replies / Z drafts / N escalated)

Why cycle_day is a strict mapping

Early testing showed Claude was flexible enough to interpret cycle_day loosely — once it picked Day 3 "last nudge" for a cycle_day=2 venue. The prompt was tightened to an explicit table: cycle_day=1 → Day 1 template, cycle_day=2 → Day 2, cycle_day=3 → escalate (no draft). No skipping allowed.

7Three Outcomes Per Venue

Draft created
cycle_day < 3, no reply in thread
create_followup_draft(...)
update_venue_row({
  cycle_day: +1,
  last_action_date: today
})
Resolved
any message in thread from external address
update_venue_row({
  status: "resolved",
  resolved_date: today
})
Escalated
cycle_day == 3, still no reply
update_venue_row({
  status: "escalated"
})
→ included in Slack summary

8Tracking Sheet Schema

ColHeaderFormatWritten by
Athread_idGmail thread hexintake (never rewritten)
Bvenue_namefree textintake
Csender_emailemailintake
Dcycle_day1–3agent updates
Estatusactive · resolved · escalatedagent updates
Flast_action_dateYYYY-MM-DDagent updates
Gthread_linkURLintake
Hcreated_dateYYYY-MM-DDintake
Iresolved_dateYYYY-MM-DDagent updates (on resolve)
Jvenue_emailemailintake

The agent only ever writes D, E, F, I. A/B/C/G/H/J are set at intake (row creation) and never modified.

9Secrets & Environment

Secrets (wrangler secret put)
ANTHROPIC_API_KEY — Claude key
GOOGLE_CLIENT_ID — OAuth app
GOOGLE_CLIENT_SECRET — OAuth app
GOOGLE_REFRESH_TOKEN — Alec's gmail+sheets scopes
SLACK_WEBHOOK_URL — #make_team webhook
Vars (wrangler.toml)
SHEET_ID
SHEET_TAB = "Tracking"
RUN_MODE = "draft" (flip to "send" later)
CLAUDE_MODEL = claude-sonnet-4-5-20250929
AGENT_VERSION = 0.1.0

The Google refresh token mints a fresh access token on each Worker invocation via oauth2.googleapis.com/token. Cached in-memory for the duration of a single request (~60 min typically). Tokens survive Worker cold starts because the refresh token lives in Secrets, not memory.

10Sample Run — 5-Venue Timeline

T+0.0s
Cron fires (or /run hit)
Worker wakes. Claude client constructed. Access token refreshed from Google.
T+1.2s
Turn 1 → Claude: "run the cycle"
Claude requests get_active_venues. Worker dispatches, returns 5 rows. ~1.6K system tokens cached.
T+3.5s
Turn 2 → Claude decides per venue
Claude requests check_thread_replies for all 5 thread_ids in parallel.
T+8s
Turn 3 → Branch decisions
Silverbrook + Willowdale → resolve. Harbour + Meadowbrook → draft. Clifftop → escalate.
T+20s
Turn 4 → Writes executed
update_venue_row + 2× create_followup_draft fired. Gmail + Sheets respond 200.
T+32s
Turn 5 → Final summary
Claude calls send_slack_alert: "5 checked · 2 replies · 2 drafts · 1 escalated". end_turn.
T+35s
Worker returns
Summary JSON returned to caller: 14 tool calls, 35.6s, $0.078, 1583 output tokens.

11Cost Breakdown

Token typeRate (Sonnet 4.5)Sample run (5 venues)Cost
Input$3 / 1M15,254$0.0458
Output$15 / 1M1,583$0.0237
Cache read$0.30 / 1M8,230$0.0025
Cache write$3.75 / 1M1,646$0.0062
Per run26,713$0.0781
Monthly (30 runs)~$2.34

Versus Make.com: old scenarios cost ~100 operations/day = 3000 ops/month = close to the 60K/month plan limit. Agent runs in a different budget (Anthropic API, not Make.com operations) so frees up all 100 daily ops for other automations.

12Deployment Topology

💻
Local dev
wrangler dev
📦
wrangler deploy
uploads bundle
☁️
Cloudflare Worker
venue-agent.alec-3c0.workers.dev
Cron
daily 01:00 UTC
Worker endpoints
GET /health — liveness probe (no auth)
GET /run?key=<last16> — manual trigger
scheduled(event) — cron entrypoint
Observability
wrangler tail — live log stream
Cloudflare dashboard → Workers → Logs
FlowTest /venue-agent page — manual trigger UI + last-run stats

13Failure Modes & Recovery

FailureDetectionRecovery
OAuth refresh failsrunAgent throwsindex.ts catches, posts ⚠️ Slack alert, returns error JSON
Single tool call errors (transient)runToolWithRetryAuto-retry once after 500ms
Single tool call errors (hard)is_error:true in tool_resultClaude continues, skips venue, notes in Slack summary
Claude rate-limit429 response from AnthropicAnthropic SDK retries internally; agent errors on exhaustion
Runaway loopturn counter30-turn safety cap in agent.ts
Bad prompt outputDrafts caught in Drafts folderHuman review — nothing auto-sent in RUN_MODE=draft
Sheet schema driftupdate_venue_row writes to wrong columnManual test via /run; check sheet diff before trusting cron

14The System Prompt (Live)

This is the full prompt Claude sees on every run. Stored in src/prompts.ts — edit and redeploy to change behaviour.

You are the Venue Follow-Up Agent for Easy Weddings.

Your job, once per day at 12pm Sydney time:
1. Call get_active_venues to fetch every venue awaiting a reply.
2. For each venue, call check_thread_replies with the thread_id.
3. Decide:
   - REPLIED — if any message in the thread is from an address NOT in our
     internal list (alec@, lauren@, travel@easyweddings.com.au). Call
     update_venue_row with status="resolved" and resolved_date=today.
   - NEEDS FOLLOW-UP — if only internal messages, and cycle_day is 1 or 2.
     Draft a follow-up using create_followup_draft, then update_venue_row
     with cycle_day += 1 and last_action_date = today.
   - ESCALATE — if cycle_day is 3 and still no reply. Call update_venue_row
     with status="escalated". Send ONE combined Slack alert at end of run.
4. End of every run: ONE Slack summary to #make_team (X checked / Y replies /
   Z drafts / N escalations). Keep it under 5 lines.

Rules:
- NEVER send emails directly — only create drafts. A human reviews before sending.
- The draft must reply inside the original thread (same subject with "Re: ").
- Template selection is a STRICT mapping from cycle_day:
    cycle_day=1 → use Day 1 template, then bump cycle_day to 2
    cycle_day=2 → use Day 2 template, then bump cycle_day to 3
    cycle_day=3 → DO NOT DRAFT. Escalate. Mention in Slack summary.
- If a tool errors, retry it once. If it fails again, skip that venue and
  include the failure in the Slack summary.

15Build History

Phase 1
Scaffold
wrangler.toml, tsconfig, package.json, 6 TypeScript files totalling ~630 lines.
Phase 2
Tool layer + Google API
OAuth refresh, Gmail threads/drafts, Sheets batch read/update. RFC2822 builder.
Phase 3
Prompt + templates
System prompt, Day 1/2/3 templates ported from Make.com scenario.
Phase 4
Secrets + deploy
5 secrets uploaded via wrangler secret put. Worker live at venue-agent.alec-3c0.workers.dev. Cron registered.
Phase 4.5
Schema fix
First run revealed tools.ts had wrong column mapping. Fixed to match real sheet schema (A-J verified against actual data).
Phase 5
FlowTest integration
New /venue-agent page on FlowTest: health, manual trigger, last-run stats, prompt display, templates, tool reference. Deployed to flowtest-1m8.pages.dev.
Phase 6
5-branch end-to-end test
Seeded 5 realistic venue threads + sheet rows. Ran agent. Every branch hit correctly. Found + fixed UTF-8 em-dash bug in MIME encoder + template ambiguity in prompt.
Next
Option B — per-mailbox OAuth
When comfortable, add refresh tokens for Lauren + travel@ so drafts land in their respective inboxes instead of all in Alec's.

16Agent vs Make.com (old scenarios)

DimensionMake.com (old)Venue Agent (new)
EngineMake.com bundles + routers + filtersClaude Sonnet 4.5 tool-use loop
Triggers2 scenarios (intake + daily)1 Worker, 1 cron + 1 HTTP endpoint
Modules / steps22 (7 Set Variables alone to dodge pills bug)6 tools
PersonalisationStatic templatesPer-venue opening + closing, references details from email
Reply detectionDomain-based or Gmail search queryThread-based (any non-internal sender in thread)
Cost~100 ops/day~$0.08/run (different budget)
Deploy changesMake.com UI or API blueprint push (pills bug)Edit TS file, wrangler deploy
DebugMake.com execution logwrangler tail + FlowTest dashboard
ExtensibilityAdd modules, wire carefullyAdd a tool function + schema — Claude figures out when to call it

Generated 2026-04-19 · venue-agent v0.1.0 · FlowTest dashboard