Book an appointment without double-booking
Requirement
"The agent must book appointments and must never double-book." The client's bookings live in their own system, behind an API key.
Pieces
- Custom data: a collection
opening_hours, one record per weekday. - Connection: the booking system's key, provider
http_key. - One tool per action: an HTTP tool that fetches a day's bookings and an HTTP tool that creates one, both pointed at the connection.
- Custom tool:
free_slots, a pure calculation over the hours and the bookings. - Flow: query hours, fetch bookings, compute free slots, then an edge on the result to the booking step.
- The flow as a tool, attached to the node where booking happens, on a published agent.
This is the shape for a diary that lives in the client's own system. When the
diary is the client's Google Calendar instead, the no-double-booking rule is a
parameter of the calendar write — refuse_overlap — and not a flow at all: the
last variation below says how, and
Tools and their parameters says what those built-ins
accept and how a fixed value is changed without losing the others.
The shape to avoid
One tool that authenticates, filters out the hours the business is closed, checks availability and books. Nothing in it can be changed, tested or reused apart from the rest; the hours are wrong the first holiday; and the key sits in a tool's configuration, which the API refuses.
Steps
Ids from an earlier step are written as {{step_N.field}}; the test that runs
this page substitutes them. Every body below was sent, in this order, against
the API before this page was published.
-
Hold the booking system's key once.
grant_secretis a JSON string naming the header the key travels in; it is never returned.create_connectionCode -
The opening hours are records, not code. The collection's
query_,get_andwrite_tools exist the moment it does; their ids come back asquery_tool_id,get_tool_idandwrite_tool_id.create_collectionCode -
One record per day the business is open.
create_record(collection_id={{step_2.id}})Code -
And the next.
create_record(collection_id={{step_2.id}})Code -
One tool that reads the day's bookings. A
GETsends its parameters as the query string. It carries no header: the connection supplies the key.create_toolCode -
One tool that creates a booking. A
POSTsends its parameters as a JSON body.create_toolCode -
The one calculation no piece provides: which hours are free. It is handed the hours and the bookings and answers a value; it reaches nothing, so
declared_reachis empty. The response carriestool_id, the id nodes and flows attach; it answers "not published" until step 8.create_custom_toolCode -
Publish it. Refused if the code does not parse or defines no
transform.publish_custom_tool(custom_tool_id={{step_7.id}})Code -
The flow that composes the four.
create_flowCode -
First step: read the hours with the collection's own query tool. The first step added is the entry step. The flow's own inputs (
date,time,name) are in scope as$ref:trigger.<key>.add_step(flow_id={{step_9.id}})Code -
Second step: fetch the day's bookings for the requested date.
add_step(flow_id={{step_9.id}})Code -
Third step: the calculation. A query step answers
{"records": [...]}, so its records are$ref:<step>.records. The booking system is assumed to answer{"bookings": [...]}; a JSON object is read as it is, and any other body is wrapped as{"body": ...}.add_step(flow_id={{step_9.id}})Code -
Fourth step: the booking, only ever reached through the conditional edge.
add_step(flow_id={{step_9.id}})Code -
Hours, then bookings: unconditional.
add_flow_edge(flow_id={{step_9.id}})Code -
Bookings, then the calculation: unconditional.
add_flow_edge(flow_id={{step_9.id}})Code -
The decision. A custom tool's answer is under
result, so the field is<step>.result.requested_is_free. When it is nottruethe flow ends at step 12 and its output, the free slots, is what the agent gets.add_flow_edge(flow_id={{step_9.id}})Code -
Register the flow as one tool.
config.flow_idis what makes a tool a flow; theinput_schemais what the model sees.create_toolCode -
An agent publishes only with an AI profile. One is made from an LLM setting naming a model the platform offers (
get_ai_profile_capabilitieslists them).create_llm_settingsCode -
The profile.
create_profileCode -
The agent. It is created with an empty graph and a draft version.
create_agentCode -
The draft names the graph (
graph_id) and is the version to edit (id).get_draft(agent_id={{step_20.id}})Code -
The node where booking happens, with the one tool attached. The instructions say what to do with the answer; the procedure is in the flow.
add_node(graph_id={{step_21.graph_id}})Code -
A conversation starts at the root node.
set_root(graph_id={{step_21.graph_id}},node_id={{step_22.id}})Code -
The draft names the profile.
edit_draft(version_id={{step_21.id}})Code -
Nothing takes traffic until this. Refused, with reasons, if the graph has no root node or the draft names no profile.
publish_version(version_id={{step_21.id}})Code
What the agent sees
One tool, book_appointment, with date, time and name. The four steps,
the connection and the hours are invisible from the conversation; when the
time is taken the tool answers with the free slots and the node instructions
say what to do with them.
Variations
-
Bookings kept on the platform instead of the client's system: replace the two HTTP tools with the collection's own tools,
query_bookingsandwrite_bookings, on abookingscollection; the flow is otherwise the same. -
Confirm before booking:
requires_confirmation: trueon thebook_appointmenttool puts the call to the person before it runs. -
A second agent that books: attach the same tool id to its node. The flow, the tools and the connection are the organization's, not the agent's.
-
The diary is the client's Google Calendar: drop both HTTP tools and the custom tool.
calendar_read_busyanswers the busy ranges andcalendar_create_eventwrites the visit, each an instance of a built-in pointed at agoogleconnection, withcalendar_idandtimezonefixed on it. The overlap check comes with the write: fixrefuse_overlap: trueand a slot that collides is refused, with the ranges it collides with named, so the agent can offer another time without a flow computing anything.locationon the same tool is where the visit's address — or coordinates aslat,lng— goes, kept exactly as given and read back the same way. The opening-hours collection still earns its place: the diary says what is taken, not when the business is open.CodeA later
update_toolthat namesfixed_parametersreplaces that whole object, so adding a fourth fixed value means sending these three back with it — the trap Tools and their parameters opens with.