Tools
The Tools section of the project is where you define what the AI can do — fetch data, fire backend actions, ask the user to upload a file, render a card. Each tool you create here becomes available to the AI for matching against user messages.
This page is a field-by-field reference for the tool builder form. For the runtime side — how your mobile app handles a tool the AI invoked — see the Handling Tools guide.
Creating a Tool
- Open Project → Tools
- Click New Tool
- Fill in at minimum: Name, Description, Execution mode
- Save — the tool is enabled by default and immediately eligible for AI matching
The form is grouped into the sections below. Every field maps directly to runtime behavior described in the Handling Tools guide.
Identity & Matching
| Form field | What it does |
|---|---|
| Name | Unique key per project. The AI emits this back to the SDK to identify which tool was invoked. |
| Description | The full behavioral specification of the tool — read by the chat LLM after the tool has been selected. Include what the tool does, multi-stage flows, parameter rules, response phrasing, anything the AI needs to use the tool correctly. Up to 2000 characters. The single most important field — vague descriptions cause both false matches and incorrect execution. |
| When to use | Optional short intent hint (max 500 chars) read only by the selector (the lightweight LLM that decides which tool to invoke). Use it when the full Description is long and you want a concise signal for tool routing. Leave empty if Description’s first lines already make intent clear. The chat LLM never sees this field. |
| Category | Grouping label — purely organizational, not seen by the AI. |
| Tags | Free-form tags the AI sees alongside the description. Useful for adding extra matching signals. |
| Usage examples | Example user phrasings that should fire this tool. Treated as positive training signals at matching time. |
Description vs When-to-use
Tool invocation runs in two LLM phases, and the two fields target different phases:
| Phase | What runs | Reads description? | Reads whenToUse? |
|---|---|---|---|
| Selector | A small, fast LLM picks which tool (if any) fits the user’s message. | First line only (fallback) | Full text (preferred when set) |
| Chat | The main LLM runs the selected tool — extracts parameters, follows your protocol, writes the user-facing reply. | Full text | Not used |
Practical guidance:
- Always fill
Descriptionthoroughly. This is what governs how the tool actually behaves at runtime — multi-stage approval flows, “ask for missing detail before confirming” rules, response wording, etc. - Fill
When to useonly when the Description is too long for the selector to skim quickly. Two-three short bullet-style lines of “fire when…” / “skip when…” are ideal. AddingwhenToUsedoes not change chat-phase behavior — it only sharpens the selector. - A common mistake is treating
whenToUseas additive context for the main chat LLM. It isn’t. If the chat LLM needs to know something, it goes inDescription.
Parameters
In the Parameters section, declare each input the AI should extract for this tool. For every parameter:
| Form field | What it does |
|---|---|
| Name | The key the AI emits back when calling the tool (e.g. productId). Used as-is in the SDK’s onToolSuggested callback under tool.params. |
| Type | string, number, or boolean. |
| Required | If on, and the AI can’t extract a value, the AI will ask the user a follow-up question to get it. |
| Description | Tells the AI what to extract or generate. The single most important field after Name — be specific. |
| Default value | Used when the AI doesn’t extract a value. A required parameter skips the follow-up prompt if it has a default. Supports template variables — see Template Variables below. |
| Label | Human-readable display name shown in the dashboard and used by email actions. Falls back to Name when empty. |
The AI pulls parameters from two places: the user’s message and whatever the SDK passes in its runtime context object. There’s no per-parameter “pull this key from context” wiring — if a value lives in context with a clear name, the AI will pick it up automatically.
Template Variables
Some parameters need values the AI shouldn’t generate — random tracking numbers, IDs, timestamps, fields pulled from the SDK’s userContext. Writing rules like “generate a random 10-digit number” in the description doesn’t work well: LLMs have heavy sample bias when asked for random values (you’ll see the same digit prefixes recurring). For these cases, write a template variable in the parameter’s Default value field — the server resolves it at runtime, then injects the resolved value into the AI’s prompt with a “use exactly this value” instruction.
Syntax: {{namespace.key}} (mustache style — distinct from the single-brace {paramName} substitution used in endpoint URLs).
| Token | Resolves to |
|---|---|
{{system.uuid}} | UUID v4 (e.g. 9f3e2c1b-...) |
{{system.id10}} | 10-character URL-safe nanoid (e.g. k7d2N9mPxQ) |
{{system.digits10}} | 10 random digits (e.g. 4829105736) |
{{system.trackingDefault}} | Default tracking ID format TRK-<base36 epoch>-<4 hex> (e.g. TRK-LM3K8F2A-9C2F) — time-prefixed, CX-friendly, used as the fallback when Tracking ID Format is left empty. |
{{system.timestamp}} | ISO 8601 timestamp |
{{system.ymd}} | Date in YYYY-MM-DD format |
{{user.<key>}} | Any field from the SDK’s runtime userContext (e.g. {{user.firstName}}, {{user.userId}}) |
{{tool.trackingId}} | The tool’s resolved tracking ID for this invocation — see Tracking ID Format. The same value is reused everywhere it’s referenced (description, email subject prefix, webhook URL, headers, body) so a single complaint carries one ID end-to-end. |
Example: A complaint-tracking tool needs a stable reference number that’s unique per case and shown to the user in the chat reply. Set the parameter:
Name: trackingNo
Type: string
Default value: {{system.digits10}}At runtime the server generates 4829105736, tells the AI “trackingNo is set to 4829105736 — use this exact value”, and the AI emits the same value in params.trackingNo plus naturally references it in the user-facing reply (“Your tracking number is 4829105736”). The same value is reused across all turns of the conversation (memoized per session) so the user never sees it change.
Use sites: Template variables work in any string field admins write — currently the parameter Default value, the email action’s Subject prefix, and the endpoint URL. More fields (webhook bodies, headers, deep links, loading messages) gain support as the system rolls out.
What to know:
- Recursion is disabled — values produced by token resolution aren’t re-scanned for tokens. Users can’t smuggle template syntax through their chat messages.
{{user.*}}only reads keys the SDK already passed inuserContext— no extra DB lookups, so adding a template token never widens the data the server can see.- If the AI emits a different value than the server-generated one, the server silently overrides with its own value (and logs a warning so prompt drift can be detected).
Endpoint (Optional)
If the tool maps to a backend HTTP call, switch on Endpoint and fill in:
| Form field | What it does |
|---|---|
| Method | GET, POST, PUT, DELETE, or PATCH. |
| URL | The endpoint URL. Parameter names in {curly} braces get substituted from the extracted parameters (e.g. https://api.example.com/products/{productId}). |
| Body template | For POST/PUT, the JSON shape to send. Same {paramName} substitution rules apply. |
| Required context | A list of keys that must exist in the SDK’s runtime context for this tool to be eligible. Acts as a gate — if any required key is missing, the AI never sees the tool that turn. Useful for tools that only make sense on certain screens (e.g. require productId so the tool only fires on a product detail page). |
Execution Mode
The biggest behavioral choice — pick the mode based on where the tool actually runs and who writes the final reply.
| Mode | Who runs the tool | Who writes the final reply | Use when |
|---|---|---|---|
| Custom (default) | The mobile app, in onToolSuggested | The mobile app, via addResponse(data, tool) | The tool result is purely a UI render — show a card, open a modal, navigate. AI doesn’t need to know what came back. |
| Server | Qafka backend | AI, in a follow-up turn | Tool needs a real backend (DB, third-party API) and the AI should weave the result into prose. The SDK shows a one-line bridge (“On it…”) while the backend works. |
| Custom with AI | The mobile app, in onToolSuggested | AI, in a follow-up turn | Data lives only on the client (e.g. local DB, user-entered form) but you want the AI to summarize it into a natural reply. SDK posts your result back to the backend for a second LLM turn. |
The chosen mode is injected into the system prompt so the AI knows whether to write a final reply itself or just a bridge sentence.
Multi-Step Flows
Some tools collect data over several turns (e.g. “book an appointment” needs date, then service, then user confirmation). In the Steps section, add a row per milestone:
| Form field | What it does |
|---|---|
| Key | Step identifier (e.g. date_selected, service_selected, complete). |
| Description | What this step represents. The AI uses it to decide when to emit the step. |
| Required fields | Parameter names that must be collected for this step to fire. The step triggers as soon as the AI has all of them. |
| Actions | Optional side-effects to run when this step completes — same action types as below (email / webhook). Useful for “send a confirmation email when complete fires”. |
Step completion is reported to the SDK via the onStepCompleted callback.
File Input & Document Extraction
If the tool needs a file from the user (receipt photo, PDF invoice, etc.), switch on File Input and fill in:
| Form field | What it does |
|---|---|
| Accepted MIME types | Which file types are allowed (e.g. image/jpeg, image/png, application/pdf). |
| Max size | Upper bound in bytes. |
| Sources | Which pickers the SDK should offer — any combination of camera, gallery, file. |
| Webhook delivery format | Default file delivery format for webhook actions: url (signed URL), base64 (inlined), or formdata (multipart). The webhook action’s own File Delivery Format setting overrides this; this field is the project-wide fallback. |
To run AI extraction on the uploaded file, also switch on Document Extractor and add:
| Form field | What it does |
|---|---|
| Prompt | Free-form instruction to the extraction LLM (e.g. “Extract invoice fields from this document”). |
| Fields | The structured fields you want pulled out — name, type (string / number / boolean), and whether each is required. |
After upload, the backend runs the extraction LLM and emits structured data via the SDK’s onExtractionResult callback.
Backend Actions
In the Actions section, add side-effects that fire when the tool completes (or when a specific step completes — see Multi-Step Flows). Two action types:
| Form field | What it does |
|---|---|
| Targets | One or more email + condition rows. Single target with no condition behaves like the simple “send to one address” case. Add multiple targets to route the same email to different inboxes based on per-target conditions — see Conditional Actions below. |
| Subject prefix | Prefix added to the auto-generated subject line. |
| Include conversation | Append the chat transcript to the email body. |
| Attach file | If the tool collected a file, attach it. |
| User context keys to include | Explicit allow-list of keys from the SDK’s runtime context to render as a “User Info” section. Anything not on this list never leaves the system — KVKK / GDPR boundary. Default: empty (nothing forwarded). |
| Labels | Optional human-readable display labels for those keys (e.g. firstName → "Name"). |
| Action condition | Optional natural-language condition. When set, the AI decides whether the entire action should fire. See Conditional Actions. |
When the tool has a Tracking ID Format set (or the legacy params.no parameter), the email body’s details block auto-renders a tracking-number row with the resolved value at the top — no admin config required. To surface it in the inbox listing as well, include {{tool.trackingId}} in the Subject prefix.
Webhook
Webhook actions POST a JSON (or multipart) payload to a partner endpoint — typically a CRM, ticketing system, or workflow automation. Every webhook delivery is recorded in Action Logs with a full request/response snapshot so you can audit, debug, and reproduce locally.
URL & Method
| Form field | What it does |
|---|---|
| URL | Endpoint to call. Supports template tokens — {{tool.trackingId}}, {{user.userId}}, etc. — resolved at runtime. So https://crm.example.com/cases/{{tool.trackingId}} becomes https://crm.example.com/cases/SHK-8984690258 for that specific complaint. |
| Method | POST or PUT. |
Custom Headers
Add arbitrary headers as key/value pairs. Three common uses:
- Authentication —
Authorization: Bearer xxx,X-API-Key: sk_live_.... Required for almost every real CRM. - Routing metadata —
X-Source: qafka,X-Environment: production. Helps the receiver classify or filter incoming requests. - Tokenized values — header values support the same template tokens as the URL. Writing
X-CRM-Reference: {{tool.trackingId}}puts the per-invocation tracking ID in a header so the receiver can route or de-duplicate without parsing the body.
Custom headers always win over the auto-set Qafka headers below — useful when you need to override User-Agent or replace the auto-attached metadata.
Auto-Set Qafka Headers
Every webhook request also carries these headers without admin config — receivers can rely on them for identification, idempotency, and signature verification:
| Header | Value | Purpose |
|---|---|---|
User-Agent | Qafka-Webhook/1.0 | Standard identification in the receiver’s access log. |
X-Qafka-Tool | The tool’s name | Lets the receiver branch by tool without parsing the body. |
X-Qafka-Tracking-Id | The tool’s tracking ID for this invocation | Mirrors the body’s trackingId field — useful for routing layers (load balancers, edge functions) that don’t read the body. |
X-Qafka-Delivery-Id | A fresh UUID per request | Idempotency key. If your receiver retries downstream work, key it on this value to avoid duplicating cases when Qafka itself retries. |
X-Qafka-Timestamp | Unix seconds | Replay-window check. Reject requests older than your tolerance. |
X-Qafka-Session-Id | The chat session ID | Correlate multiple invocations from the same user session. |
X-Qafka-Signature | sha256=<hex> | Only sent when an HMAC secret is configured — see below. |
Payload
| Form field | What it does |
|---|---|
| Include extraction data | Sends the AI-extracted fields under the extraction key (e.g. { category, brief_summary, severity }). On by default. |
| Include uploaded file | Sends the file the tool collected. Effect depends on the File Delivery Format below. No-op when the tool has no file input or no file was uploaded. |
| Include conversation history | Sends the full chat transcript under conversation. Useful for support tools where the receiver needs to read what the user said; verbose otherwise. |
The payload always includes a top-level trackingId field when the tool has a tracking ID set, regardless of these toggles. Receivers should rely on it for case correlation rather than parsing extraction.no (the legacy field).
File Delivery Format
When Include uploaded file is on, this dropdown picks how the file is sent:
| Format | What goes on the wire | When to use |
|---|---|---|
url (default) | JSON body with file: { url, fileName, mimeType, size }. The url is a signed GCS URL the receiver downloads from. | Smallest payload; the receiver fetches the file asynchronously. Recommended for most CRMs. |
base64 | JSON body with file: { data: "<base64>", fileName, mimeType, size }. The whole file is inlined. | Self-contained — no second HTTP round-trip. Cost: ~33% size overhead and no streaming. Use only for small files or receivers that can’t make outbound calls. |
formdata | multipart/form-data body. The file is a binary part; extraction, userContext, conversation, and trackingId ship as JSON-string parts. | Legacy receivers (PHP, classic web frameworks) that expect form upload semantics. |
User Info
| Form field | What it does |
|---|---|
| User context keys | Allow-list of keys from the SDK’s runtime userContext to include in the webhook payload as a userContext object (e.g. { userId: "1825", currentShoppingMallName: "..." }). Empty by default — without this, the receiver knows what was complained about but not who complained. Same KVKK / GDPR boundary as the email action. |
Unlike the email action, webhook user context uses the raw key names (no localized labels) — receivers code against stable identifiers.
HMAC Signing Secret (optional)
When set, the runtime computes sha256(secret, payload) and sends the result in the X-Qafka-Signature header. The receiver verifies with the same secret to detect tampering and replay attacks. Recommended for any public-facing webhook; optional for internal CRMs.
The signed bytes are the JSON body for JSON-mode requests; for multipart requests the runtime signs a canonical formdata:<deliveryId>:<json-of-fields> string the receiver can recreate from the parsed parts. Implementation example for receivers:
const expected = 'sha256=' +
createHmac('sha256', SHARED_SECRET)
.update(rawRequestBody)
.digest('hex');
if (req.headers['x-qafka-signature'] !== expected) {
return res.status(401).end();
}Rotate the secret periodically and prefer per-target secrets over a global one if the same Qafka project sends webhooks to multiple receivers.
Action Condition
Optional natural-language condition. AI decides whether to trigger this webhook based on the request context. See Conditional Actions.
Action results — success / failure / message — are delivered to the SDK via onActionResult. The full request and response snapshots (including masked auth headers, response status, response body) are captured in Action Logs for forensic auditing.
Conditional Actions
Both action types support an Action condition — free-form natural-language text the AI evaluates against the tool’s parameters, the SDK’s userContext, and the conversation. If the condition isn’t met, the action is skipped and an entry is written to Action Logs with the AI’s reasoning. Examples:
"Only for complaint messages"— fire only when the AI judges the user is complaining"currentRegion = Europe"— deterministic field-equality form (admin friendly)"For users on the premium plan"— fuzzy semantic condition the AI resolves fromuserContext
The same evaluation happens in the same LLM call that produces the tool parameters — no extra round-trip, no extra latency. When no actions on a tool have conditions (and email actions are single-target), the condition decision payload is omitted from the prompt entirely, so cost is zero for tools that don’t use the feature.
Multi-Target Email Routing
A common case for email actions: route a single tool to different inboxes based on tenant or context. Add multiple Targets with per-target conditions; AI picks the matching ones at runtime. Single tool, single action, N inboxes.
Example — one support tool routing to different teams:
| Target email | Target condition |
|---|---|
| billing@example.com | ”User is asking about billing or invoices” |
| tech@example.com | ”User is reporting a technical issue” |
| sales@example.com | ”User is asking about pricing or upgrading” |
| … (more) | … |
Targets are evaluated independently — write mutually-exclusive conditions for routing-style cases (one inbox), inclusive conditions for fan-out cases (notify multiple teams). When the AI cannot match any target on a multi-target email, the action is skipped (no broadcast) and the skip is logged. This is intentional KVKK behavior — empty conditions cannot leak data to all recipients by accident.
Per-target conditions also work with a single target: the AI decides whether that one address gets the email, equivalent to setting an action-level condition. Use whichever placement is more readable for the admin.
UI Rendering
The Response Configuration section tells the chat widget how to render the tool’s response. The outer settings (response type, data path, max items, layout, position) apply to BOTH renderer modes; the only choice is which renderer draws each item.
Common settings
| Form field | What it does |
|---|---|
| Response type | list, detail, card, table, or summary — controls iteration. list plus an array payload renders one card per item. |
| Data path | Dot-path into the response payload to find the actual items (e.g. data.stores). Leave empty when the array is at the root. |
| Max items | Cap for list mode — extra items render as a “…and N more” footer. |
| Layout | vertical (stack) or horizontal (scrollable row). |
| Position | before (above the assistant message) or after (below). |
Item renderer — pick one
| Mode | When to use |
|---|---|
| Component (rendered in app) | Your React Native app already has a polished component (StoreCard, ProductRow, …). Set “Item component” to its registry key and the SDK looks it up at render time. See Customizing Components for the SDK-side wiring, or run qafka sync to auto-generate a stub file with the right data shape, doc comment, and <Qafka components> registration. |
| Card (designed here) | No app code needed. Visually compose a layout from the primitive whitelist (QView, QText, QImage, QIcon, QDivider, QButton) using the JSON editor. AI generate + Modify + iPhone-frame live preview keep the loop fast. |
Card mode
⚠️ Requires
@qafka/react-nativev1.1.0 or newer. Older SDK versions don’t know how to render a card and will silently fall through to the default renderer (plain text or basic list). Make sure your mobile app ships at least v1.1.0 before flipping a tool to Card mode in production.
Selecting Card opens an inline editor inside the section:
- Slug + Label — internal identifier and human-readable name. Slug is unique per tool.
- Card definition (JSON) — the schema is
{ schemaVersion: 1, root: <node> }. Each node hascomponentplus its props at the same level (noprops: {…}wrapping). UsefieldNameto bind text/image leaves to fields in your tool result. Conditionally show ornaments withshowIf. - Sample data — paste a real example of what your tool returns. The preview binds against this; when you save the tool, the sample data persists alongside the card definition so the editor re-hydrates next session and the AI generate flow has a real shape to design around.
- Live preview — wrapped in an iPhone 16 Pro chrome so you see the card in roughly the same proportions partners will. Buttons inside the preview are inert; click handlers fire only inside the SDK.
- AI designer —
Generateproduces 2 variants (Compact + Detailed) from the tool description and sample data;Modifyapplies a natural-language change to the current JSON;↶ Undowalks back through the last 10 AI edits.
Card buttons can fire one of seven CTA types: external_navigation, deep_link, suggest_message, copy, dismiss, share, tool_trigger. The SDK handles copy, dismiss, share, suggest_message, and external_navigation internally with sensible defaults. deep_link and tool_trigger cross the SDK boundary — the partner app registers a callback (see Card CTAs on the SDK side).
Tracking ID Format
The Tracking ID Format field (in the right sidebar, under Settings) generates a unique identifier per tool invocation. The same ID is shared across every action of that invocation — email subject, body, webhook payload, webhook headers, the AI’s user-facing reply, and the action log row — so one user-visible reference number traces a single transaction end-to-end.
Set it once on the tool. Reference it anywhere admin-authored strings live via the {{tool.trackingId}} token.
| Behavior | What happens |
|---|---|
| Empty | Runtime auto-generates TRK-<base36 epoch>-<4 hex> per invocation (~13 chars, time-prefixed, CX-friendly). Use this when you don’t care about the format. |
| Custom format | Whatever template string you write is resolved per invocation. Must contain at least one dynamic token ({{system.*}} / {{user.*}}); a static string would produce the same ID for every complaint. The validator rejects format strings without tokens at save time. |
Legacy params.no | Tools that shipped before this field existed and used parameters[name="no"] with a templated defaultValue keep working — the runtime promotes that value to the tracking ID when the format field is empty. New tools should use the format field directly and skip the no parameter. |
Examples
| Format | Example output | Use case |
|---|---|---|
| (empty) | TRK-LM3K8F2A-9C2F | Default — anything tracking-shaped |
SHK-{{system.digits10}} | SHK-8984690258 | Customer-facing complaint number, easy to read aloud |
RES-{{system.id10}} | RES-K7vN2pBxR9 | Reservation reference, alphanumeric |
{{user.userId}}-{{system.digits10}} | 1825-8984690258 | Per-user tracking, helpful when one CRM has multiple sources |
TKT-{{system.ymd}}-{{system.id10}} | TKT-2026-05-01-K7vN2pBxR9 | Daily-grouped tickets |
Where it shows up
- AI replies — write
{{tool.trackingId}}inside the tool’s Description (e.g. “Your complaint has been filed under reference{{tool.trackingId}}”) and the AI quotes the same ID in its user-facing message. - Email body — auto-rendered as a tracking-number row at the top of the details block; the admin doesn’t need to touch the email config to surface it.
- Email subject — write
{{tool.trackingId}}inside the Subject prefix to put the ID in inbox listings (e.g.[Complaint] {{tool.trackingId}}). - Webhook payload — sent as a top-level
trackingIdfield on every webhook delivery, regardless of the action’s other config. - Webhook headers — sent as
X-Qafka-Tracking-Idautomatically; admin headers can also reference{{tool.trackingId}}(e.g.X-CRM-Reference: {{tool.trackingId}}). - Action log — stored in the
tracking_idcolumn with a B-tree index, so the Action Logs search box returns the row sub-millisecond.
Uniqueness
Tracking IDs are generated fresh per invocation — different complaints in the same chat session get different IDs (a session-wide cache earlier collapsed every complaint to one ID; that bug is fixed). Within a single invocation, every reference resolves to the same string by construction (resolved once when the prompt is built, then propagated). The IDs are not DB-unique; collision probability is governed by the random space of the format you choose. For {{system.digits10}} (10⁰¹⁰), birthday-paradox first collision at ~100k complaints, way past practical workloads. If you need stricter guarantees, add a unique index in your own migration.
Risk & Confirmation
| Form field | What it does |
|---|---|
| Risk level | low, medium, or high. Medium and high tools are surfaced separately in the dashboard’s risk reports; high-risk tools are flagged for the AI in the system prompt. |
| Requires confirmation | When on, the AI is instructed to ask the user “Are you sure?” before emitting the tool call. |
Custom Data
The Custom Data field is a free-form JSON blob that travels with the tool to the SDK. Use it to pass partner-specific configuration (apiBaseUrl, brand strings, feature flags) so the mobile app’s onToolSuggested handler can stay generic across tools.
Loading Message
| Form field | What it does |
|---|---|
| Loading message | Shown in the chat (text mode) or as a voice pill (voice mode) while async work is in flight. Single string or an object keyed by locale. |
Enabled
Toggle a tool off without deleting it. Disabled tools are not sent to the AI for matching.