Field notes · AI security

← Writing

An 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 dedupe when the query hits, and skips with reason no-approval unless both CAL_API_KEY and CAL_EVENT_TYPE_ID are set. Every skip is logged to the execution log — never silent.[3]
  • Booking allowed? An IF node routes allow to 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"

← Back to Writing
Book a call →
human-in-the-loop-booking-automation