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 answerFor 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.
| Mode | SDK callback fires | Your addResponse should | Final reply written by |
|---|---|---|---|
custom (default) | Yes — onToolSuggested | Render the result and close out | Your app (the rendered UI itself is the reply) |
server | No — backend handles it; SDK just streams a bridge sentence | n/a | AI, in a follow-up turn |
custom-with-ai | Yes — onToolSuggested | Return the data with mode 'data' so the SDK posts it back to the backend | AI, in a follow-up turn |
For a field-by-field walkthrough of how to configure each mode in the dashboard, see Dashboard › Tools › Execution Mode.
Handling Tools in the SDK
Wire onToolSuggested on the widget to receive matched tools at runtime:
<Qafka
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.tsdispatcher with a typedToolHandlerper tool, generates parameter destructuring +addResponsepayload skeletons that match each tool’s dashboarddataPath, and keeps everything in sync as you add tools.onToolSuggestedthen 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:
| Mode | Updates UI | Commits to flow | Use 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
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
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
onExtractionResult={({ toolId, data, status, incompleteFields }) => {
if (status === 'incomplete') {
// Ask the user to fill missing fields
promptFor(incompleteFields)
} else {
saveExtraction(data)
}
}}
/>onActionResult
Fires when backend actions (email, webhook) finish executing:
<Qafka
onActionResult={(results) => {
results.forEach((r) => {
if (r.data?.skipped) {
// Action was skipped because its conditional firing rule didn't match —
// not an error, just informational. r.data.reason holds the AI's rationale.
return
}
if (!r.success) console.error(`${r.actionType} failed: ${r.message}`)
})
}}
/>For tools with conditional actions, a result with success: true and data.skipped: true means the action was eligible to run but the AI’s evaluation of its condition (or the multi-target email’s targets) said “don’t fire.” Treat this as informational, not as a failure.
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
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:
- The tool is enabled in the dashboard
- The user’s message matches its description well enough that the AI picks it
- 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.