Skip to main content
A tool is a name, an input schema, and the shape of the answer. The assistant calls it when the owner’s question needs it, your server answers with data, and Tappify draws the card.
Leave the flags off and the command asks what the tool answers with and how expensive it is to run. It writes 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

The command writes this entry. You can hand-edit 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

Tappify posts 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 and Asking 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.
As the owner types, Tappify calls 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 need ai: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.