server/tools/compare.ts and schemas/compare.input.json, adds the handler to
your server’s tools map, adds the ai:tools scope, and gives the manifest a server block if
it has none.
The manifest entry
tappify.extension.json instead — the
$schema line gives your editor completion and validation, and
tappify extension doctor checks the result.
description is the only thing the assistant has to go on. Write it as one sentence about what
the tool answers, in the owner’s words rather than yours: “Compares two cohorts in the funnel”
beats “Runs the cohort comparison endpoint”.
The assistant reads the title and description strings inside input too, so they are held to
the same rules as the tool description: they are scanned at publish, they are cut to 400
characters before the assistant sees them, and each one is at most 500 characters in the manifest.
Describe the argument, the way you would in your own API reference; an instruction addressed to
the assistant fails the publish scan on a public extension.
With cache, a repeat call carrying the same input for the same install is answered from
Tappify’s own cache for that window and your server never sees it. The key is the install, the
tool and the input — not the owner who asked — so two owners of the same install share one
window. Don’t personalise a cached answer by claims.userId; leave cache off for a tool whose
answer differs per owner.
The handler
POST /tappify/tools/compare with the body every host-to-vendor call carries:
request.input typed from your schema, request.context, request.credentials and
request.documents. A tool call has no owner filter behind it — the model asked, not a dashboard
— so request.context.filters carries the instant of the call and the neutral platform and
country. Answer within 30 seconds and under 1 MB; a tool call is never retried.
What each returns value must answer with
A
value is a number or a string you have already formatted; a point’s y is a number and its
x is a label.
An answer missing a key its returns promises comes back as TOOL_RESPONSE_INVALID and the
assistant is told the tool did not answer. validateToolResponse from
@tappify/extension-sdk/testing checks the same keys in your own tests.
The keys are only the gate. Tappify refuses an answer that is missing one, and the host then
reads each value it kept: a points entry that is not an [x, y] pair with a numeric y, a
label that is not a string, a column without a key, and the card falls back to one sentence.
So passing the validator means the keys are there, not that the card will draw — the values still
have to hold the shapes in the table above.
What the owner sees
A row in the chat carrying your tile andAsking Funnel Lab while the call runs, Asked Funnel Lab once it comes back, then the card Tappify draws from your answer: your tile in the header,
and one owner action, Pin as widget. A pin keeps a snapshot of that card on the owner’s Home
page; the snapshot is stored in their own browser, so it is theirs alone, and Unpin takes it
off again.
A card whose data does not match the kind it promised draws one sentence saying so, and no
action. Nothing you send reaches the transcript as markup: the host reads only the keys in the
table above and draws them itself.
What the assistant reads back
The owner’s card is drawn from your whole answer. What goes back to the assistant is separate: your answer is serialised into a<vendor_content kind="tool_result"> block that says it came
from your extension, and cut to 8,192 characters. The assistant is told a block like that is the
vendor’s data, not an instruction to it — so an answer that asks the assistant to do something
reads as a sentence about what you want, and nothing more. Keep the answer to the keys the card
needs: anything past them spends the budget without reaching the owner.
Adding a mention list
A mention list puts your own objects in the owner’s@ picker.
GET /tappify/mentions/funnels?q= under your
server.baseUrl — the route createTappifyHandler serves for every name in mentions. The
manifest requires a list field and the command fills it with that same path; the host does not
read it.
Answer with { items: [{ id, label }] }. Tappify keeps the first 20 items, cuts each label to 80
characters, and holds your answer for a minute per install, list and query, so a keystroke storm
costs you one call. Any other shape is MENTION_RESPONSE_INVALID, and a name the live release
does not declare is MENTION_NOT_DECLARED.
When the owner picks an item, the turn carries your extension, the list, the item’s id and the
label the owner saw. That is also what makes a context provider declared when: ["mention"] run
for that turn.
Limits
Tool calls are capped at 600 an hour per project, counted per workspace for an organization-scoped install. Fifteen tools, ten mention lists and ten actions per extension; both tools and mentions needai:tools.
Tappify takes the 95th percentile of your tool calls over 90 days and publishes it on your
listing’s quality panel. Over four seconds the listing is flagged, and a flagged extension is one
the assistant stops offering: it still answers about you and still calls your tools when an owner
asks for you by name, but it no longer proposes you unasked.