> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thoughtly.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Voice agent scheduling guide

> Production patterns for mid-call scheduling with Thoughtly Actions and Automations — handle bookings, reschedules, and cancellations across calendar tools.

This guide shows production patterns for scheduling **inside the agent** using mid-call [Actions](/agents/actions).

***

## Before you start

### 1) Connect your scheduling tool

Go to [Integrations](/integrations/getting-started) and connect your scheduler so it is available in both:

* [Agent Builder](/agents/overview) -> [Speak node](/agents/nodes#speak-node) -> [Actions](/agents/actions)
* [Automations](/automations/getting-started) -> Steps ([Automation actions](/automations/actions))

<Frame caption="Integrations screen with Calendly and Cal.com connected">
  <img src="https://mintcdn.com/thoughtly/ImdkAObRlPeZXHig/images/ui/scheduling-integration-ui.jpg?fit=max&auto=format&n=ImdkAObRlPeZXHig&q=85&s=0be0046af3deddced1d1382a8157e2d7" alt="Integrations screen with Calendly and Cal.com connected" width="1204" height="522" data-path="images/ui/scheduling-integration-ui.jpg" />
</Frame>

***

## Key building blocks (how scheduling works)

### Variables (capture the caller's date/time)

[Variables](/agents/variables) extract **immediately after the caller's latest reply and before outcome evaluation**, so your routing can validate the date/time right away.

If you loop back to the same node, variables **re-extract and overwrite** prior values (perfect for "try again" date collection). See [Loops](/agents/outcomes#loops-special-use-case).

<Frame caption="Variables panel with a &#x22;preferred_datetime&#x22; variable configured">
  <img src="https://mintcdn.com/thoughtly/ImdkAObRlPeZXHig/images/ui/scheduling_variable.jpg?fit=max&auto=format&n=ImdkAObRlPeZXHig&q=85&s=4f9862ac5f6612425cbbf48b1ab2e946" alt="Variables panel with a &#x22;preferred_datetime&#x22; variable configured" width="310" height="549" data-path="images/ui/scheduling_variable.jpg" />
</Frame>

### Actions (run scheduling mid-call)

In our agent, [Actions](/agents/actions) run mid-call from a [Speak node](/agents/nodes#speak-node). When Actions exist, the node can **auto-proceed without waiting for another caller reply**, and [rule-based outcomes](/agents/outcomes#rule-based-outcomes-deterministic) are recommended because outcomes fire based on internal values/results.

<Frame caption="Speak node Actions panel with a scheduling action selected">
  <img src="https://mintcdn.com/thoughtly/ztZivwRw5I1RzxPg/images/ui/loop-call-contact.png?fit=max&auto=format&n=ztZivwRw5I1RzxPg&q=85&s=44597ab1d1366df3f3aa3c38298431e5" alt="Speak node Actions panel with a scheduling action selected" width="613" height="543" data-path="images/ui/loop-call-contact.png" />
</Frame>

### Scheduling timezone handling (important)

Scheduling related Actions accept an optional `timezone`. If not set, Thoughtly falls back to:

1. timezone input (action config) -> 2) agent timezone (advanced settings) -> 3) `America/New_York`.

See [Calendly timezone handling](/integrations/scheduling/calendly#timezone-handling) for details.

***

## Pattern A (recommended default): Caller picks a date/time, then book it

### Flow overview

1. **Ask for preferred date** (Speak -> Prompt)
2. **Check availability mid-call** (Speak node -> Get Available Times Action)
3. **Present returned slots and collect a choice** (Speak -> Variable + loop to Step 2 if needed)
4. **Book the selected slot** (Speak node with Scheduling Action)
5. **Confirm** (Message / Prompt)
6. **Fallback** (offer alternatives or [Transfer](/agents/nodes#transfer-node))

***

### Step-by-step (Agent Builder)

#### Step 1 - "Collect preferred date" Speak node

Create a [Speak node](/agents/nodes#speak-node) that asks something like:

* "What day works best for you?"
* "If you have a timezone preference, tell me as well."

Create [Variables](/agents/variables) on this node:

* `preferred_date` (Text)
* `preferred_timezone` (Text, optional)

**Extraction instruction suggestion (copy/paste style):**

```text theme={null}
Goal: extract the appointment date the caller states.
Output: YYYY-MM-DD.
If absent or unclear: return empty.
Do not invent values.
```

<Frame caption="Speak node to collect date">
  <img src="https://mintcdn.com/thoughtly/AAFHJrRLyxdzgi5d/images/ui/automations/scheduling-step1.jpg?fit=max&auto=format&n=AAFHJrRLyxdzgi5d&q=85&s=bfb3d7a80c9ce481c4315526936773fc" alt="Speak node to collect date" width="1280" height="735" data-path="images/ui/automations/scheduling-step1.jpg" />
</Frame>

Route directly to **Step 2** from this node. Keep loops in Step 3, where slot selection happens.

#### Step 2 - Check availability node (mid-call Action)

Create a new [Speak node](/agents/nodes#speak-node) to run availability lookup:

* **Message:** "Let me check what times are open."
* Add a scheduler **Get Available Times** action ([Calendly](/integrations/scheduling/calendly) or [Cal.com](/integrations/scheduling/cal-com))
* Map input values from Step 1:
  * Requested date: `preferred_date`
  * Timezone: `preferred_timezone` (optional)

<Frame caption="Speak node to check timeslots via Action">
  <img src="https://mintcdn.com/thoughtly/AAFHJrRLyxdzgi5d/images/ui/automations/scheduling-step2.jpg?fit=max&auto=format&n=AAFHJrRLyxdzgi5d&q=85&s=7a09a44790ddb569f31cb932bc31e76e" alt="Speak node to check timeslots via Action" width="1280" height="791" data-path="images/ui/automations/scheduling-step2.jpg" />
</Frame>

#### Step 3 - Collect preferred slot from returned availability

Create another Speak node that presents the returned slots and asks the caller to pick one:

* "I have 10:00 AM, 2:30 PM, or 4:00 PM. Which works best?"
* Extract a variable such as `selected_time` from the caller's reply
* Add outcomes on this node:
  * If `selected_time` is valid -> continue to **Step 4 (Booking)**
  * If no slot is selected, caller wants another time, or caller provides a different date -> loop back to **Step 2 (Check availability)** and run lookup again

<Frame caption="Speak node to provide available timeslots">
  <img src="https://mintcdn.com/thoughtly/AAFHJrRLyxdzgi5d/images/ui/automations/scheduling-step3.jpg?fit=max&auto=format&n=AAFHJrRLyxdzgi5d&q=85&s=b16a6d8c979dc22801a8e87b88074f07" alt="Speak node to provide available timeslots" width="1280" height="665" data-path="images/ui/automations/scheduling-step3.jpg" />
</Frame>

**Prompt suggestion (copy/paste):**

```text theme={null}
timeslots: response

Your task is to provide a few (no more than 3) available time slots from the list, based on the person's request. If the timeslots is empty just say: "I don't have any openings for this day, should I check another one?"

Only provide and talk about information that is available in the timeslots list. If it is empty, do not come up with information; simply let the customer know that you don't have an opening for that date.
```

#### Step 4 - Booking Speak node (Actions)

Create a new Speak node:

* **Message:** "One moment while I book that for you."
* Add an **Action** for your scheduler ([Calendly](/integrations/scheduling/calendly) or [Cal.com](/integrations/scheduling/cal-com)) and map:
  * Date/time input: `selected_time`
  * Timezone: `preferred_timezone` (or rely on the Calendly fallback order)

**Authoring tip:** disable interruptions for mid-call Actions so the caller does not interrupt during booking.

<Frame caption="Calendly &#x22;Schedule appointment&#x22; action mapping fields">
  <img src="https://mintcdn.com/thoughtly/ztZivwRw5I1RzxPg/images/ui/field-mapping.jpg?fit=max&auto=format&n=ztZivwRw5I1RzxPg&q=85&s=9605c39ebda29722f3a0785ceb0fd6b5" alt="Calendly &#x22;Schedule appointment&#x22; action mapping fields" width="840" height="748" data-path="images/ui/field-mapping.jpg" />
</Frame>

#### Step 5 - Confirm vs error (rule-based Outcomes after booking)

In the **same booking node**, add [rule-based outcomes](/agents/outcomes#rule-based-outcomes-deterministic) that check Action outputs such as:

* `booking_status == "confirmed"` -> confirmation node
* Else -> error-handling node (try different time / get availability / transfer)

<Frame caption="Speak node to book and do error handling">
  <img src="https://mintcdn.com/thoughtly/AAFHJrRLyxdzgi5d/images/ui/automations/scheduling-step5.jpg?fit=max&auto=format&n=AAFHJrRLyxdzgi5d&q=85&s=0f21af96f4462d9e88685b9da9dca694" alt="Speak node to book and do error handling" width="1280" height="630" data-path="images/ui/automations/scheduling-step5.jpg" />
</Frame>

#### Step 6 - Confirmation Speak node

Use **Message** mode if you want an exact script:

* "You're all set for \[date/time]. You'll receive a confirmation shortly."

<Frame caption="Confirmation node with verbatim message enabled">
  <img src="https://mintcdn.com/thoughtly/ztZivwRw5I1RzxPg/images/ui/confirm-end-node.jpg?fit=max&auto=format&n=ztZivwRw5I1RzxPg&q=85&s=94bfa139a224b2a4fd137b5ea4d083cd" alt="Confirmation node with verbatim message enabled" width="310" height="439" data-path="images/ui/confirm-end-node.jpg" />
</Frame>

***

## Pattern B (pre-call availability): Prefetch times in Automations, book faster mid-call

This pattern reduces mid-call complexity by fetching available times before the live conversation starts, then using those times as metadata during the call.

It is ideal when you want:

* Faster conversations
* Fewer mid-call API round trips
* Simpler agent logic
* More deterministic scheduling flows

### How it works

#### 1) Pre-call Automation

Use a **Get Available Times** action inside an [Automation](/automations/getting-started), then store the returned times in metadata before you trigger the call.

* Add a scheduling availability step (Calendly / Cal.com)
* Store output as metadata (for example: `available_times`)
* Start or route to your call only after this step succeeds

```text theme={null}
available_times = {{ steps.calendly_get_available_times.slots }}
```

<Frame caption="Pre-call automation flow that prefetches available times (Get Times Node)">
  <img src="https://mintcdn.com/thoughtly/bPECysxsAlSYwG9r/images/ui/automations/scheduling-pattern-b-automation-prefetch.jpg?fit=max&auto=format&n=bPECysxsAlSYwG9r&q=85&s=28e4787958f47fa15f137dd1a1a58eaa" alt="Pre-call automation flow that prefetches available times (Get Times Node)" width="1280" height="719" data-path="images/ui/automations/scheduling-pattern-b-automation-prefetch.jpg" />
</Frame>

<Frame caption="Pre-call automation flow that prefetches available times (Call Phone Number Node)">
  <img src="https://mintcdn.com/thoughtly/bPECysxsAlSYwG9r/images/ui/automations/scheduling-pattern-b-automation-prefetch-1.jpg?fit=max&auto=format&n=bPECysxsAlSYwG9r&q=85&s=1edf5635968344cf9748ea0ada42dac8" alt="Pre-call automation flow that prefetches available times (Call Phone Number Node)" width="1280" height="718" data-path="images/ui/automations/scheduling-pattern-b-automation-prefetch-1.jpg" />
</Frame>

#### 2) Agent conversation

During the live call, the agent reads options from `available_times`, presents a short list, and captures the caller's selected slot.

* Present only times that already exist in metadata
* Capture selection in a variable such as `selected_time`
* Confirm the chosen slot out loud before booking

<Frame caption="Agent conversation that presents prefetched slots and captures the selected time">
  <img src="https://mintcdn.com/thoughtly/bPECysxsAlSYwG9r/images/ui/automations/scheduling-pattern-b-agent-options.jpg?fit=max&auto=format&n=bPECysxsAlSYwG9r&q=85&s=dd975dd83b293ffa2eccfb4f0bec7acf" alt="Agent conversation that presents prefetched slots and captures the selected time" width="1280" height="718" data-path="images/ui/automations/scheduling-pattern-b-agent-options.jpg" />
</Frame>

#### 3) Booking Action

In the scheduling Action, set **Reference Node** to **None** and map the selected time directly from metadata/variables instead of calling availability again mid-call.

* Reference Node: `None`
* Booking time input: `selected_time` (or equivalent mapped value)
* Keep booking outcomes rule-based (`confirmed` vs fallback path)

<Frame caption="Booking action configured with Reference Node = None and direct slot mapping">
  <img src="https://mintcdn.com/thoughtly/bPECysxsAlSYwG9r/images/ui/automations/scheduling-pattern-b-booking-action.jpg?fit=max&auto=format&n=bPECysxsAlSYwG9r&q=85&s=ab2df7904831bf22cdcd686e418578d9" alt="Booking action configuration with reference node disabled and selected slot mapped directly" width="1280" height="717" data-path="images/ui/automations/scheduling-pattern-b-booking-action.jpg" />
</Frame>

### Why this reduces friction

Because availability is already known:

* The agent does not need to fetch availability mid-call
* There is no back-and-forth waiting on additional API checks
* The conversation feels faster and more natural
* The in-call routing logic is much simpler

### Important limitation

This approach works best for near-term scheduling windows (typically within a week). If callers often request far-future dates, Pattern A (real-time availability lookup) is still recommended for accuracy.

### When to use Pattern B

Use pre-call availability when:

* Speed and conversational simplicity are top priorities
* You want fewer live integrations running during calls
* Booking windows are short-term and predictable
* You prefer deterministic slot presentation over open-ended date parsing

This gives you two production-ready scheduling approaches:

* **Pattern A**: Flexible, real-time booking with full date parsing
* **Pattern B**: Faster, lower-friction booking using prefetched availability

Both can coexist in production depending on your use case.

***

## Tips and tricks (prompting + reliability)

### Use a strict date-only extractor for validation loops

This keeps your validation clean when the caller is vague or revises their date.

```text theme={null}
Print ONLY one line with the ISO date (YYYY-MM-DD). No labels, no prose, no JSON.
You are given ONLY the latest caller message and TODAY’S DATE (CURRENT_DATE) in the America/New_York timezone. Extract exactly one date the caller intends for scheduling and output ONLY that date in YYYY-MM-DD. No other text.

Rules
1) Prefer the most specific date mentioned. If multiple are given, choose the earliest that matches the caller’s intent modifiers (e.g., “late”, “end of”).
2) If the mentioned date would be in the past relative to CURRENT_DATE, roll it forward to the next logical occurrence (e.g., same month/day next year, or the next instance of that weekday).
3) If the caller is vague (e.g., “some time next week”, “next month”, “this week”, “what do you have available”), ALWAYS produce a date by applying the mapping below.
4) Assume ISO weeks start Monday; “weekend” = Saturday–Sunday. If a fallback lands on a weekend and no weekend was requested, use the next business day (Mon–Fri).
5) Output must be a valid calendar date within the next 365 days. If your first choice falls outside, choose the nearest valid alternative that respects the intent.

Relative-Date Mapping (examples; always relative to CURRENT_DATE)
- “today” → CURRENT_DATE
- “tomorrow” → CURRENT_DATE + 1 day
- “day after tomorrow” → CURRENT_DATE + 2 days
- Bare weekday (“Friday”) → next occurrence of that weekday after CURRENT_DATE
- “this <weekday>” → that weekday in the current week; if already passed, use the same weekday next week
- “this week” / “sometime this week” → the soonest remaining day this week after CURRENT_DATE
- “next week” / “sometime next week” → Monday of next week
- “weekend” / “this weekend” → upcoming Saturday
- “next weekend” → Saturday of next week
- “this month” → the earliest remaining day this month after CURRENT_DATE
- “next month” / “sometime next month” → the 1st business day of next month
- “early <month>” → 5th of that month; “mid <month>” → 15th; “late <month>” → 25th
- “end of <month>” → last calendar day of that month
- “in N days/weeks/months” → add N with standard calendar arithmetic (weeks = 7 days; months add by month, clamping to month end if needed)
- “a couple of weeks” → 14 days
- Ordinal day without month (“the 15th”) → the next 15th on the calendar (this month if still upcoming, else next month)
- Month/day without year → this year if still upcoming; otherwise next year
- No date intent / open-ended (“what do you have available”, “ASAP”, “whenever”) → next business day after CURRENT_DATE
```

### Use a full datetime extractor right before booking

This captures the final intent, including confirmation of a proposed slot.

```text theme={null}
The final scheduled datetime the caller intends, returned as YYYY-MM-DDTHH:MM:SS (24h).
From FULL_CONVERSATION, extract exactly one datetime the caller intends for scheduling. Output ONLY one string in the format YYYY-MM-DDTHH:MM:SS and nothing else.


Conversation logic
1) Consider only the caller’s latest clear intent. If the caller revises the time/date later, the latest revision wins.
2) If the agent proposes a slot and the caller affirms (e.g., “yes”, “works”, “sounds good”), use that proposed slot.
3) Ignore tentative or rejected options that are later superseded.

Date resolution (vague → concrete)
- “today” → CURRENT_DATE
- “tomorrow” → +1 day
- Bare weekday (“Friday”) → next occurrence after CURRENT_DATE
- “this <weekday>” → that weekday in the current ISO week (Mon–Sun); if already past, use next week
- “this week” / “sometime this week” → soonest remaining business day this week after CURRENT_DATE
- “next week” → Monday next week
- “weekend” / “this weekend” → upcoming Saturday
- “next weekend” → Saturday next week
- “this month” → earliest remaining day this month
- “next month” → 1st business day of next month
- Ordinal day without month (“the 15th”) → next 15th (this month if upcoming, else next month)
- Month/day without year → this year if upcoming, else next year
- “in N days/weeks/months” → add N with calendar arithmetic (weeks=7 days; months clamp to month end)
- “early/mid/late <month>” → 05/15/25 of that month
- If fallback date lands on weekend and caller didn’t ask for weekend, use next Monday.

Time resolution (vague → concrete, 24h)
- Exact times → parse as given (handle “am/pm”).
- “noon”/“midday” → 12:00:00
- “midnight” → 00:00:00 (on the chosen date)
- “morning” → 10:00:00
- “early morning” → 08:00:00
- “afternoon” → 15:00:00
- “evening” → 18:00:00
- “late evening” / “tonight” → 20:00:00
- “EOD” / “end of day” → 17:00:00
- “lunchtime” → 13:00:00
- “quarter past X” → X:15:00; “half past X” → X:30:00; “quarter to X” → (X-1):45:00
- If no time is given, default to 10:00:00 (business-friendly).
- If the chosen time falls outside business days and caller did not indicate off-hours/weekend, keep the date but set time to 10:00:00 next business day.

Edge cases
- If caller is completely open-ended (“what do you have available”, “whenever”), choose next business day at 10:00:00.

Output
Return exactly one line in this format: YYYY-MM-DDTHH:MM:SS
No labels, no prose, no JSON, no quotes.
```

### Offer only real availability

Use this prompt when you’re reading from an availability response to avoid hallucinated slots.

```text theme={null}
timeslots: {{response}} 

Your taks is to provide a few (no more than 3) available time slots from the list, based on the person's request. If the timeslots is empty just say: "I don't have any openings for this day, should I check another one?"

Only provide and talk about information that is available in the timeslots list, if it is empty then do not come up with information, simply let customer know that you don't have an opening for a date.
```

## See also

* [Actions](/agents/actions) - mid-call integrations and execution order
* [Variables](/agents/variables) - extraction and overwrite behavior
* [Outcomes](/agents/outcomes) - rule-based vs prompt-based routing
* [Automations](/automations/getting-started) - prefetch and data passing
* [Calendly](/integrations/scheduling/calendly) - action details and timezone handling
* [Cal.com](/integrations/scheduling/cal-com) - action details
