Skip to content
GitHub
Get started →

Knowledge & lifecycle tools

The remaining 4 tools cover everything that isn’t page perception or page action: searching your knowledge base, capturing leads, ending the call, and managing conversation-flow state for multi-step scripted interactions.

search_knowledge_base({ query })

Search your crawled site content (FAQs, blog posts, services pages, anything indexed) for a specific factual answer.

{ "query": "DUI defense attorney" }

When the AI calls it: specific factual questions the AI can’t answer from the current page — “do you do flat roof repair?”, “what are your prices?”, “do you handle DUI cases?”. The system prompt tells the AI not to call this for navigation, scrolling, or questions about the current page (use read_page instead).

Returns: JSON the model can parse:

{
"results": [
{
"content": "We handle all categories of DUI defense including first-offense, refusal, and felony DUI...",
"url": "/services/dui-defense",
"section": "Practice areas"
},
{ "content": "...", "url": "/about", "section": "Experience" }
]
}

Top 4 chunks ranked by pgvector similarity over your crawled content.

What it’s NOT

search_knowledge_base is the default LLM-callable search in every Spelo bundle. It hits your crawled site content — pages indexed when you ran “Crawl my site” from the dashboard.

It is not the same as search_database (which queries a connected DB adapter — Postgres, Shopify, Airtable, etc.). See:

You want to search…Call…Available on…
Your crawled site content (FAQs, blog, services pages)search_knowledge_baseEvery bundle, every plan
A connected database (Postgres, Shopify, Airtable, etc.)search_databaseDFY tier + voice-relay transport; or via authenticated REST /query call from your own backend

If you have both a crawled KB and a connected database, the AI uses search_knowledge_base for prose-style answers and search_database (on DFY) for structured filtering.

Auto-navigate-and-read pattern

The system prompt encourages a 3-step pattern when the visitor asks a content question:

1. search_knowledge_base({ query: "..." }) → { results: [{ url, section }] }
2. AI speaks a one-sentence answer + offers to read more
3. If visitor says "yes" → read_section({ url, section_id }) → full prose + highlight + scroll

This gives a search engine + audio reader in one — answer first, deep-dive on demand.

Server-side path (voice-relay transport)

When the bundle is configured for our managed voice infrastructure (sites using Gemini, or free-tier overflow), search_knowledge_base is handled server-side by apps/voice-relay/src/tools.ts instead of in the browser. Same wire format, same return shape; only the dispatch path differs. Customers don’t need to care which transport is active.

submit_lead({ data })

Send captured visitor contact info + qualifying answers to your dashboard. Triggers webhook events if configured.

{
"data": {
"name": "Priya Patel",
"phone": "+1 415 555 0142",
"email": "priya@example.com",
"interested_in": "DUI defense consultation"
}
}

When the AI calls it: once per call, only after every REQUIRED field listed in the LEAD CAPTURE block of the system prompt has been captured and read back to the visitor for confirmation. Field keys must match the ids in that block exactly.

Returns: "ok" on success, or "error: <code>[ missing=<fields>]" on failure (e.g. "error: missing_required missing=phone,email").

Side effects:

  1. POST /v1/<siteId>/leads with HMAC-signed body (X-Spelo-Signature) — same auth path as /query
  2. Visible toast in the bottom-right of the visitor’s screen — dark pill confirming ”✓ Got your details” with the captured fields. Auto-dismisses after 8 s. Lets the visitor catch and correct misheard digits.
  3. Webhook fireslead.captured event sent to every webhook subscriber configured in the dashboard. See Webhooks.

Availability: only present in the LLM tool list when the site has lead capture enabled. If lead capture is off, the tool is not in the schema and the AI cannot accidentally call it.

Configure lead capture fields and notification routing in Dashboard → your site → Settings → Leads.

end_call

Gracefully terminate the voice call when the visitor wants to stop.

{}

When the AI calls it: “goodbye”, “bye”, “end the call”, “that’s all”, “I’m done”, “hang up”, “talk to you later”. The system prompt instructs the AI to always speak a short closing line first (“Thanks, have a great day!”) and only then call this tool. Never call it after every reply — only when the visitor explicitly wants to stop.

Returns: "ok" immediately (synchronously, before the actual teardown happens).

Why synchronous return matters: the tool-result message must land on the data channel before the WebRTC connection is torn down. If we returned a Promise that disconnected first, the OpenAI Realtime API would see the call as abandoned mid-tool and emit an error. The actual disconnect is deferred by a setTimeout(..., 0) tick so the tool-output message reaches OpenAI cleanly.

Side effects:

  1. Mic track stopped, WebRTC peer closed, audio element destroyed
  2. Pill UI transitions to “Call ended” state
  3. Session row in the database flips to ended_at = now
  4. Conversation row created (if not already) with full transcript

Fallback: if no onEndCall hook is wired (older bundles), the tool returns "ok (no disconnect hook)" and the bundle keeps running until the silence timeout fires (~30 s). The agent thinks it ended; visitor’s mic stays on for half a minute. Not great, but not destructive.

set_flow_state({ state })

Transition the conversation to a new state in your scripted conversation flow (only present when the site has a flow configured).

{ "state": "qualification" }

A conversation flow is a finite state machine you define in the dashboard. Each state has a goal (instruction the AI follows while in that state) and a list of next states it can transition to. Example for a multi-step booking flow:

greet → qualify → collect_details → confirm → done
decline → done

When the AI calls it: when the goal of the current state has been satisfied. E.g. after qualify confirms the visitor has the right intent, the AI calls set_flow_state({ state: "collect_details" }) to move on.

Returns:

  • "ok — now in state \"collect_details\"" on success
  • "error: cannot transition from \"qualify\" to \"confirm\". Allowed: collect_details, decline" if the requested target isn’t in the current state’s next list
  • "error: no conversation_flow configured for this site" if flows aren’t enabled

Client-side validation: the transition rules are evaluated in the browser (no API round-trip), so the AI gets immediate feedback. The bundle maintains the current state in memory and updates it only on successful transitions.

Configure flows in Dashboard → your site → Settings → Conversation flow.

See also

  • Personality + custom instructions — the base system prompt the AI follows when no flow is configured
  • Webhooks — receive lead.captured, conversation.ended, session.started events
  • Conversations API — fetch transcripts including every tool call after the fact
  • Action toolsact.confirm is the safety rail in front of submit_lead when a flow demands explicit consent