> ## 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 a row action

> Put an entry in a host table's row menu that opens your component with that row.

A row action adds an entry to the row menu of one of Tappify's tables. Choosing it opens
your component in the expand panel with the row as `tap.context`.

```bash theme={null}
tappify extension add row-action --name "Explain rank" --table analytics.keywords
```

## The manifest entry

```json theme={null}
{
  "contributes": {
    "rowActions": [
      {
        "id": "explain-rank",
        "title": "Explain rank",
        "entry": "src/row-actions/explain-rank.tsx",
        "table": "analytics.keywords"
      }
    ]
  }
}
```

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.

The four host tables are `analytics.keywords`, `analytics.reviews`,
`deployments.releases` and `deployments.builds`. They are listed with their columns on
[Pages and slots](/extensions/reference/pages-and-slots).

## The component

```tsx theme={null}
import {
  TapButton,
  TapCard,
  useTap,
  useTapServer,
  type TapRowActionProps,
} from "@tappify/extension-sdk";
import "@tappify/extension-sdk/styles.css";

export default function ExplainRank({ row }: TapRowActionProps) {
  const tap = useTap();
  const keyword = typeof row.keyword === "string" ? row.keyword : "";
  const explanation = useTapServer("explainRank", { keyword });

  return (
    <TapCard
      title={`Why "${keyword}" moved`}
      footer={
        <TapButton
          variant="ghost"
          size="sm"
          onClick={() => void tap.ui.copy(explanation.data?.summary ?? "")}
        >
          Copy
        </TapButton>
      }
    >
      {explanation.data?.summary}
    </TapCard>
  );
}
```

`row` arrives as `Record<string, unknown>`, which is why `keyword` is narrowed before it is
used. `explanation.data` is typed once `tappify extension types` has turned
`server.procedures` into declarations — see
[Generated types](/extensions/build/generated-types).

The same object arrives as `tap.context`, so a component that is both a widget and a row
action can read one place: `useTapContext()`.

## What the host renders

Your entry appears in the row menu below Tappify's own entries, with the vendor tile.
Choosing it opens the expand panel at 640 pixels; Esc closes it, and the footer's
Open in your product is the only exit out of Tappify. The row is passed read-only — a row
action that changes the owner's data does it through an
[action](/extensions/reference/scopes) with an approval card, never by writing directly.
