Foyer

Scope guardrails

How the agent refuses what you have not configured it to accept. Configurable per business, validated at write time, enforced by the agent state machine and by server-side checks on every action that touches the calendar.

The shape of a scope config

Every business has a scope object stored against its row. The fields are flat, validated as a unit on write, and read on every turn by the agent worker.

{
  "service_types": [
    { "key": "drain-clear", "label": "Drain clearing", "est_duration_min": 90 },
    { "key": "leak-repair", "label": "Leak repair", "est_duration_min": 120 },
    { "key": "water-heater-install", "label": "Water heater install", "est_duration_min": 240 }
  ],
  "service_area": {
    "kind": "zip-list",
    "zips": ["80301", "80302", "80303", "80304", "80305", "80306", "80307", "80308", "80309", "80310"]
  },
  "business_hours": {
    "mon": [["08:00", "17:00"]],
    "tue": [["08:00", "17:00"]],
    "wed": [["08:00", "17:00"]],
    "thu": [["08:00", "17:00"]],
    "fri": [["08:00", "17:00"]],
    "sat": [],
    "sun": []
  },
  "blocked_dates": ["2026-07-04", "2026-12-25"],
  "min_lead_minutes": 120,
  "max_lead_days": 21,
  "quiet_hours_start": "21:00",
  "quiet_hours_end": "08:00",
  "timezone": "America/Denver",
  "human_handoff_phone": "+13035550100",
  "human_handoff_threshold": 0.55
}

Validation at write time

PATCH /v1/businesses/:id/scope applies these checks before anything is persisted, and surfaces them inline in the Filament UI:

  • service_types non-empty; keys unique; durations positive.
  • service_area non-empty — at least one ZIP, or a center point and a non-zero radius.
  • business_hours non-empty across the week; intervals well-formed, end after start.
  • min_lead_minutes < max_lead_days * 1440.
  • quiet_hours_start and quiet_hours_end form a valid wrapping or non-wrapping range.
  • timezone resolves to a real IANA zone.
  • human_handoff_threshold between 0 and 1; default 0.55 — below this the agent escalates rather than guessing.

Garbage configs cannot be saved. The owner does not get to send the agent into the field with no business hours and a negative lead time.

Enforcement at agent time

The agent is a state machine constrained by the scope. The phases are:

greet
  → identify_service
  → collect_address
  → collect_timing
  → collect_details (optionally photos)
  → propose_slot
  → request_human_confirm
  → confirm_to_customer
  → completed

Any phase  → abandon
Any phase  → out_of_scope
Any phase  → human_handoff

At each turn the agent receives the scope and the prior conversation state. The structured output schema includes the chosen next phase. The agent cannot emit a phase that is not in the enum — parser rejects it, retry up to twice, third failure escalates to human_handoff and dead-letters the turn.

Enforcement at write time

The agent proposing an out-of-scope slot is one failure mode; the agent persisting an out-of-scope booking is another. Foyer defends in depth:

  • Slot search filters by business hours, blocked dates, lead-time bounds, and quiet hours before any candidate goes to the LLM.
  • The hold-creation path re-validates: the proposed slot is inside business hours, the address is in the service area, the service type is in the catalog. Mismatch is a 422 with a problem document — the agent re-tries from propose_slot.
  • The confirm endpoint re-validates one more time before the Calendar lock. A stale hold that drifted out of policy is rejected.

Quiet hours specifically

Quiet hours apply to outbound, not inbound. A customer can text at midnight; the agent will queue the response for the next allowed window. Confirmation messages obey the same rule — a 9pm confirmation becomes an 8am text. The default 21:00 – 08:00 customer-local range is FCC-aligned; per-business override is supported.

The out-of-scope log

Requests that hit the fallback are persisted to out_of_scope_log with the inferred reason (out of area, out of hours, service not offered, request unparseable). The Filament admin surfaces patterns as service-expansion suggestions — if a third of refused requests are for water-heater installs, the dashboard says so. The clustering is basic keyword grouping; the suggestion is for the owner to think about, not for the agent to act on.

See also Operations runbook for the kill-switch and per-business cost ceilings, which are the two higher-order overrides on top of scope.