Skip to main content
An action is anything that changes the world outside Tappify. Every one of them creates a run that starts pending, shows the owner a card, and only reaches your server after they approve.

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.
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. 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.
The promise resolves as soon as the run exists. A run Tappify refuses to create rejects instead: with a 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.
A 2xx answer marks the run succeeded and whatever object you returned is stored on it. Anything else marks the run failed and records the status code you answered with, so throw a 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.

Limits and the record

Two hundred runs a day per project, counted when the run is created, so a run the owner rejects spends one too. Every run is kept with who asked, who approved, the input it was created with and what came back, and owners read their extension’s runs on its page in their workspace. The share of runs owners approve over 90 days is one of the four numbers on your listing: under 60 % the listing is flagged, with the share spelled out as the reason.