> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tappify.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Add skills and prompts

> Teach the assistant how to use your extension, and give owners one-click questions.

A skill is markdown the assistant loads when your extension is the right thing to reach for. A
prompt is a question the owner can put in the chat composer with one click, from chat, from your
widget, from your page, or under a card one of your tools produced.

```bash theme={null}
tappify extension add skill growth-review
tappify extension add prompt why-drop --surfaces chat_suggestion,widget,result_card
```

Both need the `ai:skills` scope, and that scope carries a justification the owner reads at
install.

## The manifest entry

```json theme={null}
{
  "scopes": [
    { "key": "ui:render" },
    { "key": "ai:tools" },
    {
      "key": "ai:skills",
      "justification": "The skill explains how our funnel stages are counted, so answers match our dashboard."
    }
  ],
  "contributes": {
    "ai": {
      "tools": [
        {
          "id": "compare",
          "description": "Compares two cohorts in the funnel and says which converted better.",
          "input": { "$ref": "./schemas/compare.input.json" },
          "returns": "comparison",
          "cost": "low"
        }
      ],
      "skills": [
        {
          "id": "growth-review",
          "name": "Growth review",
          "description": "How to read the funnel before reaching for a chart.",
          "file": "skills/growth-review.md",
          "tools": ["compare"]
        }
      ],
      "prompts": [
        {
          "id": "why-drop",
          "title": "Why did this drop?",
          "template": "Why did {{project.name}} lose users in the funnel last week?",
          "surfaces": ["chat_suggestion", "widget", "result_card"],
          "after": ["compare"]
        }
      ]
    }
  },
  "server": { "baseUrl": "https://funnel-lab.dev" }
}
```

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`](/extensions/test/doctor-checks) checks the result.

## Writing a skill

`skills/growth-review.md` is a procedure for the assistant, at most 4,000 words. Write the steps,
not the sales copy: which of your tools to call first, what your numbers mean, and when your data
does not answer the question.

```md theme={null}
# Growth review

Start with `compare` on the two cohorts the owner named. If they named one, compare it to the
project's median cohort.

Stage names are ours: `install` is a store install, `activated` is a first session over
30 seconds. Do not equate `activated` with Tappify's own page views.

If the range is under seven days, say the sample is short before giving a number.
```

A longer file fails publish with `bundle.skill_size`, because the assistant reads all of it the
moment it picks the skill. Reference material belongs in a knowledge file instead, which is
retrieved a paragraph at a time.

`name` is at most 60 characters and `description` 10 to 300. Together they are all the assistant
sees until it picks your skill: Tappify lists your skills by name and description on every turn
and loads the body only when the assistant selects one, so length costs you nothing until it is
used.

`tools` lists your own tool ids. A skill may only rely on the tools its own extension declares;
naming another extension's tool fails the publish with `skills.tools`.

Tappify registers your skills when the install is created, when the owner grants `ai:skills`, and
when an install is resumed, and re-reads them from the new manifest whenever one of your releases
goes live. A paused, retired or uninstalled install keeps none. At most 5 skills.

## Writing a prompt

`template` is the question, with placeholders Tappify fills in. `{{project.*}}` is resolved on
the server before the chip reaches the browser; `{{context.*}}` and `{{input.*}}` are resolved in
the owner's browser when they click.

| Placeholder                                              | Filled from                                                                              |
| -------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `{{project.id}}`, `{{project.name}}`, `{{project.slug}}` | The project the owner is in                                                              |
| `{{context.*}}`                                          | The surface the chip sits on: a widget's own context, or a page's request parameters     |
| `{{input.*}}`                                            | A form Tappify shows before filling the composer, built from the prompt's `input` schema |

Those three are the whole of `{{project.*}}`. A chip in the chat's suggestion row or under a
result card carries no context of its own, so a `{{context.*}}` there resolves to nothing — and
so does any other key Tappify does not recognise. The sentence around it is tidied up rather than
left with a gap, which is why a template reads better when the placeholder is a whole clause. A
template that leans on the surface — `Why did {{project.name}} lose users at {{context.stage}}?` —
belongs on a prompt declared for `widget` or `page`, which are the surfaces that carry one.

`surfaces` says where the chip is offered:

| Surface           | Where the chip renders                                                       |
| ----------------- | ---------------------------------------------------------------------------- |
| `chat_suggestion` | The chat's suggestion row, after Tappify's own                               |
| `widget`          | A host strip under your widget                                               |
| `page`            | Under your page or tab, and in the footer when a widget of yours is expanded |
| `result_card`     | Under a card one of your tools produced                                      |

`after` names your own tool ids, and the manifest refuses any other extension's. A `result_card`
chip is offered only under a card one of the tools in `after` produced.

`title` is at most 60 characters, `template` at most 2,000, and the resolved question a chip
carries is cut to 500. At most 20 prompts.

## Asking for input first

```json theme={null}
{
  "id": "compare-cohorts",
  "title": "Compare two cohorts",
  "template": "Compare {{input.a}} with {{input.b}} for {{project.name}}",
  "surfaces": ["page"],
  "input": { "$ref": "./schemas/compare-cohorts.input.json" }
}
```

Tappify draws that schema as a form in a host dialog, and the composer fills only once the owner
submits it. A chip that asks first carries a trailing ellipsis, so the owner knows the click opens
a form rather than filling the composer.

## What the owner sees

A chip with your tile and your `title`. Clicking it puts the finished question in the owner's
composer and stops there: they read it, edit it if they want, and press send themselves. Nothing
you write is ever sent on their behalf. Tappify offers at most two of your chips in any one row,
after its own.

When the owner does send, the turn records a `prompt_sent` against the chip it came from. A chip
naming a prompt the live release no longer declares is refused with `PROMPT_NOT_DECLARED`.

## Opening the chat from your own surface

Your widget and your page can reach the composer directly, without a declared prompt.

```tsx theme={null}
tap.nav.openChat("Why did activations fall last week?", { stage: "activated" });
```

The prompt lands in the composer, and the context object lands under it on a visible line reading
`From <your extension name>:`, which the owner can read and edit before they send. That context
is capped at 2 KB and a cut is marked with an ellipsis.

```tsx theme={null}
tap.ui.openInChat(card);
```

With `insights:write`, that puts your card into the transcript as a card the host draws, and
opens the chat drawer. Without the scope it falls back to a sentence Tappify writes about the
card, in the composer, for the owner to send.
