Field notes · AI security
← WritingAn agent that books meetings needs a human in the loop
Grey Ridge Signals Group · August 2026
An agent that books meetings needs a human in the loop
Grey Ridge Signals Group · August 2026
A lead pipeline that ends in "create a booking on the calendar" is a happy demo until you read the last node. Ours would have booked a paid event for every qualified lead — no dedupe, no confirmation, on a single environment flip. This is the gate we built before we let it.
1. The naive version
Our inbound workflow is deliberately simple: a webhook receives the lead, it is validated, parsed, and written to Postgres, then classified.[3] Qualified leads get a confirmation email and — in the original design — a real Cal.com booking created on their behalf.[3] The workflow that shipped before the guard had exactly two nodes on the qualified branch: send the email, then create the booking.[3] No dedupe, no human confirmation, no email-ownership check. One environment flip (CAL_EVENT_TYPE_ID) arms a real POST to /v2/bookings for every qualified lead that arrives, which means a double-submit is a double booking.[3]
2. The dry run that proved the path
Before anything was armed, the API contract was verified against the live Cal.com API with a zero-side-effect dry run.[1] Three probes: a well-formed body with a nonexistent event type came back 404; an empty body came back 400 enumerating the required fields; a real owned event type (5957682) with a start date in the past came back 400 — "Attempting to book a meeting in the past."[1] The third response is the interesting one: it proves the request is fully wired — auth, headers, schema, and ownership all check out — and would succeed with a valid future slot.[1] Bookings totalItems stayed 0; the path was proven without creating anything.[1]
3. The first draft was worse
The first draft of the booking node was not env-gated at all: a hardcoded URL, a hardcoded event type id, and a start time one day out.[2] A blind import of that workflow into the live container would have created real bookings for every qualified lead.[2] It was reconciled to the reviewed spec before anything shipped: the URL resolves to the real endpoint only when both CAL_API_KEY and CAL_EVENT_TYPE_ID are set, and otherwise to a local no-op; the event type id comes from the environment, never a literal; and the start time carries a seven-day buffer instead of one.[2]
4. The gate: dedupe and an approval env-gate
Three nodes now sit between the email and the booking in the live workflow.[3]
- Dedupe check. A Postgres query asks whether another qualified row exists for the same email within the last 30 days, excluding the current submission — a double-submit is caught before it reaches the calendar.[3]
- Decision. A code node skips with reason
dedupewhen the query hits, and skips with reasonno-approvalunless bothCAL_API_KEYandCAL_EVENT_TYPE_IDare set. Every skip is logged to the execution log — never silent.[3] - Booking allowed? An IF node routes
allowto the booking node and everything else to a no-booking response.[3]
The environment gate is the human gate: CAL_EVENT_TYPE_ID deliberately has no value in the repo, compose file, or CI. Until a person sets it, the workflow skips every booking with an audited no-approval reason. The guard makes the gate explicit and logged instead of a silent no-op.[3]
5. The lead is never left hanging
A skipped booking is not a dead end. The skip branch now sends a confirmation email: "We received your inquiry about AI security testing. A Grey Ridge Signals consultant will reach out within 1 business day to schedule a call." The armed path is untouched — Cal sends its own invite, so a lead never gets two emails. And the confirmation node runs with continueRegularOutput: an email failure cannot kill the run.[4]
6. Test evidence
The guard shipped with tests, written first (RED) then made green:[3]
- 45 guard checks — dedupe semantics, skip reasons, env-gate behavior, node wiring[3]
- 42 nurture-path checks — no regression on the non-qualified branch[3]
- 38 automation tests and 70 generator checks — no regression on the site pipeline[3]
- 19 pipeline-health tests — confirmation-email node present and wired[4]
All green at commit time, and re-verified for this article.[3]
7. Honest limitations
The booking node is env-gated, not auto-live. CAL_EVENT_TYPE_ID remains unset — only a person can arm it, which is the point. Until then every booking attempt is skipped and logged.[3]
The dedupe is a proxy: the leads table has no booking column, so "already has a booking" is approximated by "qualified within the last 30 days." That is a conservative superset — bookings only originate from qualified leads, so it may skip a legitimate second inquiry, but it can never double-book.[3]
The updated workflow was code-verified, not live-imported; importing into the running container is a live-stack mutation we deliberately did not perform. The guard's behavior is pinned by tests; the live flip remains a human decision.[3]
Sources
[1] https://github.com/rezearcher/greyridge-consulting/commit/b51f92dcc6d99316f55c0ebda4af5fe1e27364e2 — Cal.com POST /v2/bookings spec + patched workflow copy > "Dry run used invalid/past payloads only → 404/400, bookings totalItems stayed 0." > "booking creation is fully wired and would succeed with a valid future slot" [2] https://github.com/rezearcher/greyridge-consulting/commit/fbde3ffa3e25d32e403dc62a4a4f25fc71460f46 — Reconcile Cal booking node to reviewed spec — env-gated URL/eventTypeId, +7d start > "url": "={{ ($env.CAL_API_KEY && $env.CAL_EVENT_TYPE_ID) ? 'https://api.cal.com/v2/bookings' : 'http://localhost:9999/noop' }}" > "new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString()" [3] https://github.com/rezearcher/greyridge-consulting/commit/4d006d6c79f5fedc7f8a3e3875476245dc670a98 — GC-G1 booking guardrail — dedupe + approval gate before Create Cal.com Booking > "Booking Guard (Dedupe Check)" > "leads table has no booking column" > "skips with reason no-approval unless BOTH" > "env-gate IS the human-confirm gate" > "reason=dedupe|no-approval and stamps" > "CAL_EVENT_TYPE_ID still has no literal value" > "received_at >= now() - interval '30 days'" [4] https://github.com/rezearcher/greyridge-consulting/commit/72b34c053e92b5fcbc6db891d06ca5b24d95fe6a — Qualified-lead confirmation email on booking-skip branch > "A Grey Ridge Signals consultant will reach out within 1 business day to schedule a call." > "Qualified Lead Confirmation Email" > "We received your inquiry about AI security testing. A Grey Ridge Signals consultant will reach out within 1 business day to schedule a call." > "The booking-skip branch"