The push-notification extension
The example that runs end to end is an extension that sends a push notification through the owner’s own Firebase project. The owner supplies their Firebase credentials at install, the action asks them before every send, and their credentials reach your server and nowhere else.tappify.extension.json instead — the
$schema line gives your editor completion and validation, and
tappify extension doctor checks the result.
An action id is lowercase letters, digits and dashes, and it is the key your handler registers
under. approval is always true; there is no action that runs without one. reversible and
scope are what the card tells the owner, and the scope has to be one you also declare under
scopes. estimatesCost is declared only: the manifest accepts it and publish validates it, and
there is no way to supply an estimate, so no run carries a cost and the card shows none. At most
10 actions.
input is bounded like a tool’s: at most 16 KB serialised, and the title and description
strings inside it are read by the assistant, scanned at publish, and cut to 400 characters before
it sees them.
Asking for a run
From your own UI,tap.actions.run creates the run and returns it. Tappify draws the card; you
never do.
TapError coded TAP_SCOPE_MISSING when the install was never granted the action’s scope,
and otherwise with a TapServerError carrying Tappify’s own code — ACTION_INPUT_INVALID when
the input fails your schema, RATE_LIMIT_EXCEEDED when the project has spent its runs for the
day.
status is pending whenever the owner still has to approve. An owner can only stop being asked
for an extension whose granted scopes all sit outside security review, and ai:actions — which
every action needs — is one that does not, so an action asks every time.
Handling the run
Your handler is called once, after approval, with the validated input and the owner’s credentials.TapServerError for a failure you want a code and a sentence in your own logs for.
What the card says
The card has a slot for an
Estimated cost line, and it is never drawn: a run’s estimate is always
empty, whatever estimatesCost says.
Your description is the only line you write. Write it as a sentence an owner can act on:
Sends one push notification to the audience you pick., not Executes the send-push operation.
Where a run can start
tap.actions.run, called from your own UI, is what creates a run. Tappify posts it, draws the
card, and calls POST /tappify/actions/send-push only once the owner has approved. The extension
never sees the decision — it reads the status of the run it asked for and nothing else.
The assistant is the other way in, and it stops at the same place. When an owner’s question leads
it to one of your actions, it creates the run and the run stays pending: the owner reads the
card, and your server is called on their approval, not on the assistant’s request. Nothing the
assistant decides reaches you without an owner’s yes in between.