Skip to Content
    Qafka
    CTRL K
    CTRL K
    • Introduction
      • Quick Start (React Native)
      • Quick Start (Website)
      • React Native Configuration
      • Overview
      • npm & React
      • Options Reference
      • WordPress
      • React Native Widget
      • Headless SDK
      • Theming
      • Context
      • Navigation
      • External Navigation
      • Handling Tools
      • Voice Chat
      • Sub-Projects
      • Error Handling
      • CLI
      • Dashboard
      • Onboarding
      • Invitations
      • Billing
      • Usage
      • Settings
      • Sign-in & Account
      • Dashboard Assistant
        • Project
        • Overview
        • Conversations
        • Chat Test
        • Sub-Projects
        • Analysis
        • Unanswered Questions
        • Insights
        • Configuration
        • AI Behavior
        • Members
        • Documents
          • Overview
          • Create with AI
          • Versions
          • Response Channel
        • Action Logs
        • Navigation Rules
        • External Destinations
        • Chat Theme
        • PII Masking
        • Websites
        • Mobile Apps
        • API Keys
        • Project Settings
      • API Key Security
    • Introduction
      • Quick Start (React Native)
      • Quick Start (Website)
      • React Native Configuration
      • Overview
      • npm & React
      • Options Reference
      • WordPress
      • React Native Widget
      • Headless SDK
      • Theming
      • Context
      • Navigation
      • External Navigation
      • Handling Tools
      • Voice Chat
      • Sub-Projects
      • Error Handling
      • CLI
      • Dashboard
      • Onboarding
      • Invitations
      • Billing
      • Usage
      • Settings
      • Sign-in & Account
      • Dashboard Assistant
        • Project
        • Overview
        • Conversations
        • Chat Test
        • Sub-Projects
        • Analysis
        • Unanswered Questions
        • Insights
        • Configuration
        • AI Behavior
        • Members
        • Documents
          • Overview
          • Create with AI
          • Versions
          • Response Channel
        • Action Logs
        • Navigation Rules
        • External Destinations
        • Chat Theme
        • PII Masking
        • Websites
        • Mobile Apps
        • API Keys
        • Project Settings
      • API Key Security

    On This Page

    • How It Works
    • Execution Modes
    • Handling Tools in the SDK
    • addResponse Modes
    • Imperative loading pill
    • onStepCompleted
    • onFileUploadRequest
    • onExtractionResult
    • onActionResult
    • onToolDataRequested
    • Custom Rendering
    • Eligibility
    • Tools in Voice
    Question? Give us feedback Edit this page 
    GuidesHandling Tools

    Handling Tools

    Tools are how the AI does things inside your app — fetch data, kick off backend actions, ask the user to upload a file, render a card. The product team defines tools from the dashboard (Dashboard › Tools); the SDK handles them at runtime via callbacks on the <Qafka /> widget.

    This page covers the runtime side: lifecycle, execution modes, and the callbacks you wire up in your React Native app.

    How It Works

    User message ↓ AI matches the message against your enabled tools (using description, tags, examples) ↓ Backend emits a tool_suggestions event with the matched tool + extracted parameters ↓ Based on the tool's execution mode: • custom → SDK calls onToolSuggested → your code runs → addResponse renders / replies • server → backend runs the tool → AI streams a bridge text now, the final answer next turn • custom-with-ai → SDK runs your code → posts the result back → AI streams the final answer • staticData → backend searches the tool's uploaded data → AI streams the final answer • calendar → backend checks availability / books against a connected calendar → AI streams the final answer

    For the AI to suggest a tool, it has to be enabled, the user’s message has to match its description, and any required context keys (configured per-endpoint in the dashboard) must be present in the runtime context.

    Execution Modes

    The execution mode is set per tool from the dashboard. It dictates where the tool runs and who writes the final reply — which in turn shapes how your SDK callback is expected to behave.

    ModeSDK callback firesYour addResponse shouldFinal reply written by
    custom (default)Yes — onToolSuggestedRender the result and close outYour app (the rendered UI itself is the reply)
    serverNo — backend handles it; SDK just streams a bridge sentencen/aAI, in a follow-up turn
    custom-with-aiYes — onToolSuggestedReturn the data with mode 'data' so the SDK posts it back to the backendAI, in a follow-up turn
    staticDataNo — backend searches the tool’s uploaded data directlyn/aAI, in a follow-up turn
    calendarNo — backend checks availability and books against a connected calendarn/aAI, in a follow-up turn

    staticData searches a file you’ve uploaded for the tool; server calls your own API instead. calendar powers appointment booking. All three behave the same way from the SDK’s point of view — no callback fires, and the AI writes the reply in the next turn.

    For a field-by-field walkthrough of how to configure each mode in the dashboard, see Dashboard › Tools.

    Handling Tools in the SDK

    Wire onToolSuggested on the widget to receive matched tools at runtime:

    <Qafka // projectId, endUserId, … onToolSuggested={async (tools, addResponse) => { const tool = tools[0] const { apiBaseUrl } = tool.customData ?? {} const res = await fetch(`${apiBaseUrl}${tool.endpoint.url}`) const data = await res.json() addResponse(data, tool) }} />

    tools is an array of matched tool definitions (with their dashboard config + extracted params). addResponse(data, tool, mode?) decides what happens with the result.

    Tip — central dispatcher pattern. Inlining one big switch statement scales badly past 3–4 tools. The Qafka CLI scaffolds a handlers.ts dispatcher with a typed ToolHandler per tool, generates parameter destructuring + addResponse payload skeletons that match each tool’s dashboard dataPath, and keeps everything in sync as you add tools. onToolSuggested then just delegates: onToolSuggested={(t, a) => handleToolSuggested(t, a, ctx)}.

    addResponse Modes

    The third argument controls what the SDK does with your call. Same semantics in text and voice chat:

    ModeUpdates UICommits to flowUse case
    'both' (default)✓✓The normal path — render the result and close the tool out.
    'ui'✓✗Optimistic / progressive render. Shows a partial card, doesn’t end the flow. You must call addResponse again later with 'both' or 'data' to finalize.
    'data'✗✓Skip the UI render, just commit. In custom-with-ai mode this posts the data back to the backend. In plain custom it just clears the loading pill / unmutes the mic.
    // 1) Optimistic render then real result addResponse({ skeleton: true }, tool, 'ui') const real = await fetchData() addResponse(real, tool, 'both') // 2) custom-with-ai — return data, let the AI write the prose addResponse({ items: results }, tool, 'data') // 3) Voice flow where you only need to drop the loading pill addResponse({}, tool, 'data')

    Imperative loading pill

    For async work that doesn’t fit inside onToolSuggested (e.g. user picks an option from a custom UI you rendered, then you fetch), drive the voice loading pill manually via the widget ref:

    qafkaRef.current?.setLoading(true, 'Fetching…') const data = await doWork() qafkaRef.current?.setLoading(false)

    See QafkaHandle for the full ref API.

    onStepCompleted

    Fires whenever the AI emits a step marker for a multi-step tool (configured under Steps in the dashboard):

    <Qafka // projectId, endUserId, … onStepCompleted={({ tool, step, data, actionResults }) => { if (step === 'complete') { Toast.show('Booking confirmed!') } }} />

    actionResults lists any side-effect actions that ran with this step (e.g. confirmation email).

    onFileUploadRequest

    Fires when the AI decides the tool needs a file (tools with File Input configured). Open your own picker and call submit():

    <Qafka // projectId, endUserId, … onFileUploadRequest={({ toolId, fileInput, submit, cancel }) => { ImagePicker.launchImageLibrary( { mediaType: 'photo' }, (res) => { const a = res.assets?.[0] if (!a) return cancel() submit({ uri: a.uri!, name: a.fileName!, type: a.type! }) } ) }} />

    fileInput carries the dashboard config (accept, maxSize, sources) so you can constrain the picker accordingly.

    onExtractionResult

    Fires when document extraction finishes (only for tools with Document Extractor configured):

    <Qafka // projectId, endUserId, … onExtractionResult={({ toolId, fileId, data, status, incompleteFields }) => { if (status === 'partial') { // Ask the user to fill missing fields promptFor(incompleteFields) } else if (status === 'success') { saveExtraction(data) } else { // status === 'failed' Toast.show('Could not read the uploaded file') } }} />

    status is 'success' | 'partial' | 'failed'. fileId identifies the uploaded file the extraction ran on.

    onActionResult

    Fires when backend actions (email, webhook) finish executing:

    <Qafka // projectId, endUserId, … onActionResult={(results) => { results.forEach((r) => { if (r.success && /^Skipped:|skipped$/.test(r.message ?? '')) { // Action was skipped — either its conditional firing rule didn't match, // or (for multi-target email) the AI matched no target. Not an error, // just informational. return } if (!r.success) console.error(`${r.actionType} failed: ${r.message}`) }) }} />

    onActionResult only carries { actionType, success, message } per result — there’s no data field on the client. For tools with conditional actions, a skipped action still arrives with success: true, distinguishable only by its message: either starting with "Skipped: " followed by the AI’s reasoning (the action’s condition didn’t match), or ending with "skipped" (a multi-target email where the AI matched no target). Treat either shape as informational, not as a failure — but don’t build a hard dependency on the exact wording, since these message strings aren’t a stable, versioned contract.

    onToolDataRequested

    Fires when a suggested tool needs values for {{tooldata.X}} tokens referenced in its email/webhook body — a channel for passing transient values (e.g. PII) into an action without exposing them to the AI:

    <Qafka // projectId, endUserId, … onToolDataRequested={async ({ key, name }) => { if (key === 'sendInvoice') { const user = await store.getCurrentUser() return { gsm: user.phone, email: user.email } } return {} }} />

    Return a plain object (or a Promise of one) mapping token names to string/number/boolean values — synchronously or asynchronously. The values are posted to the backend’s request-scoped cache and substituted into the action payload when it runs; they are never sent to the AI and never written to the Qafka database. If the resolver throws or rejects, the SDK posts an empty object and any unresolved {{tooldata.X}} tokens resolve to empty strings — the action still runs, just without the substituted value.

    Custom Rendering

    Each tool can declare a custom component name in UI Config › Item component. Map that name to a real React component via the widget’s components prop:

    <Qafka // projectId, endUserId, … components={{ MyProductCard: ({ data, tool, theme, onAction }) => ( <Pressable onPress={() => onAction?.('view', data)}> <Image source={{ uri: data.image }} /> <Text>{data.name}</Text> </Pressable> ), }} />

    If the dashboard Item component value doesn’t match a registered key, the widget falls back to the default renderer for the chosen Response type (Card / List / Detail / Table). See Customizing Components for the prop signature.

    Eligibility

    For a tool to fire, three things have to be true:

    1. The tool is enabled in the dashboard
    2. The user’s message matches its description well enough that the AI picks it
    3. Any required context keys configured on the tool’s endpoint are present in the live context

    Tool support is also gated by your project’s subscription plan — if your plan doesn’t include tools, no matching happens at the backend.

    Tools in Voice

    Tools also match and fire during voice chat, but only in custom and custom-with-ai execution modes — onToolSuggested fires there the same way it does in text. server, staticData, and calendar mode tools aren’t exposed to the AI in voice sessions at all — they’re not declared in the voice tool list, so the AI has no way to call them there. Confirm any UI you build for onToolSuggested works when triggered from the voice page as well as from text chat, and don’t rely on server/staticData/calendar tools being reachable from a voice session.

    Last updated on October 1, 2026
    External NavigationVoice Chat

    © 2026 Qafka Labs OÜ