> ## 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.

# Work destinations

> Create and synchronize provider work items from mobile Review-mode collaboration threads.

A work destination turns a mobile Review-mode thread into a linked provider item. Start with
`tappify extension add work`, then implement the generated work handlers and provider webhook
adapter with `@tappify/extension-sdk@2.1.0-beta.1`.

This contract is implemented behind a disabled-by-default runtime flag. The SDK beta is published;
the later implementation's focused/runtime suites, repository release gates, and internal staging
acceptance remain pending. The examples describe the contract, not a completed production rollout.

## Review threads and customer reviews

Work destinations use the [Review SDK](https://docs.tappify.ai/sdk/how-review-mode-works.md): teammates pin feedback to a
screen in the owner's iOS or Android app and continue the conversation on the host's Review page.
`review.threads:read` grants the scoped thread snapshot, comments, screenshot descriptors, build
context, and safe device context. `reviews:read` grants App Store and Google Play customer-review
data instead. Customer reviews and public store replies are outside this work protocol.

## Declare capabilities and authentication

The following fragment shows Work Bridge's capability and scope shape. Add the usual extension
metadata and a deployed server as described in the [manifest reference](https://docs.tappify.ai/extensions/reference/manifest.md)
and [server guide](https://docs.tappify.ai/extensions/build/deploy-your-server.md).

```json theme={null}
{
  "sdk": "^2.1.0-beta.1",
  "installScope": "project",
  "scopes": [
    {
      "key": "review.threads:read",
      "justification": "Read Review thread context for linked items."
    },
    {
      "key": "work:create",
      "justification": "Create provider items from Review threads."
    },
    {
      "key": "work:sync",
      "justification": "Synchronize new comments and thread status."
    }
  ],
  "contributes": {
    "work": {
      "kind": "tracker",
      "creates": ["issue"],
      "identity": "install",
      "comments": {
        "direction": "two-way",
        "supportsEdits": false,
        "supportsDeletes": false
      },
      "status": { "direction": "two-way" },
      "attachments": ["image", "link"],
      "auth": {
        "method": "api_key",
        "fields": [
          {
            "name": "api_key",
            "label": "Fake provider API key",
            "type": "password",
            "required": true
          }
        ]
      }
    }
  }
}
```

`kind` is `tracker`, `docs`, or `chat`; `creates` contains `issue`, `page`, or `message`.
Those capabilities describe the extension. Exact provider item types come from container discovery.
Current work manifests need `review.threads:read` and `work:create`; any comment or status direction
other than `none` also needs `work:sync`. Grants must exist on the install, not only in the manifest.
Inbound writes only affect an established link under `work:sync`; there is no general extension
scope for writing arbitrary Review threads.

Each install uses one provider identity. Provider writes use that connection's credentials, even
when the content includes a teammate's display name. `identity: "user"` is rejected. Work auth is
either API-key credential fields or `{ "method": "oauth", "provider": "provider-id", "scopes": [] }`.
Provider OAuth scopes differ from Tappify scopes. Configure vendor OAuth endpoints, client
credentials, authorization/token parameters, PKCE, and resource discovery in the developer portal;
client secrets belong in the vendor vault. Install credentials remain in the host vault and arrive
on authenticated work calls. Do not put either credential set in manifests, logs, or callback records.

Comment and status directions are independent:

| Direction | Flow |
| - | - |
| `none` | No synchronization for that capability |
| `to_provider` | Tappify to the provider |
| `from_provider` | Provider to Tappify |
| `two-way` | Both directions |

The owner can turn new-comment synchronization off. Comments are append-only: edits, deletions,
reactions, and comment attachments do not synchronize. Provider comments carry the extension's
identity and the provider author's display name; they do not impersonate a Tappify teammate or
resolve provider mentions as Tappify mentions.

The SDK v2 compatibility window still accepts complete legacy manifests with static `container`,
`fields.required`, string-valued `comments`, and `status.map`, with migration warnings. Legacy
`reviews:read` validation leniency does not grant Review-thread access at runtime. Migrate to the
Review-specific scope, auth, capability objects, and dynamic metadata. Legacy `status` and `resolve`
handler routes remain deprecated; a missing `set_status` can fall back to `resolve` for resolved
targets only, with `externalItemId` adapted to `id`. Implement `set_status` for reopen as well.
Doctor checks auth, unsupported identity, scopes, deprecated fields, and a public HTTPS production
`server.baseUrl` for active sync. The CLI emits typed stubs and a webhook placeholder; it does not
implement provider delivery or durable persistence for you.

## Discover and configure a destination

After connecting, the owner selects a container on the install page. `list_containers` supports
`cursor`, `query`, and `parentId`; pass opaque cursors unchanged. `describe_container` returns the
container, item types, create fields, and current statuses for the selected container.

Each create field has an ID, label, type (`string`, `number`, `boolean`, or `string[]`), required
flag, optional choices, and optional `itemTypeIds` restrictions. The host validates supplied values
against fresh metadata. Manual configuration may omit required create values; creation still
requires valid provider fields. Automatic mode requires every applicable required value before
the host accepts configuration. Both modes require a current item type and valid exact status targets.

| Provider stage | Inbound Review state | Allowed outbound target |
| - | - | - |
| `open` | `open` | The owner's selected `openStatusId` may use this stage |
| `in_progress` | `open` | The owner's selected `openStatusId` may use this stage |
| `done` | `resolved` | The owner's selected `resolvedStatusId` must use this stage |

The host sends the exact selected `providerStatusId` on `set_status`, rather than guessing from a
label or global manifest map. A changed provider field or unavailable target needs corrected
configuration; invalid automatic configuration is rejected, and delivery validation failures can
surface as `configuration_required`.

In manual mode the thread drawer offers **Create in** for an eligible destination. In automatic
mode a newly committed thread queues creation; a failed automatic create can also be recovered
from the drawer. Existing links are returned instead of creating another item. Once linked,
comments and status follow their configured directions in either creation mode.

Accepted configuration has a `configurationRevision`. Callback provisioning has a `generation`;
secret rotation can advance the generation without changing the owner's revision. `configure`
must acknowledge both exact values and return `registeredWebhookIds`. Fence stale generations
and revisions so a delayed configure or disconnect cannot overwrite the current registration.
The host also checks accepted configuration and queued status targets when retrying.

## Implement the nine operations

Register typed handlers under `createTappifyHandler({ extensionId, work })`. Calls use
`POST /tappify/work/<operation>` and the existing authenticated install request, with current
credentials and typed `input`. See [WorkOperationMap](https://docs.tappify.ai/extensions/reference/server-api/Interface.WorkOperationMap.md)
for the full input/output contract.

| Operation | Input and typed purpose | Result |
| - | - | - |
| `configure` | `WorkConfigureInput`: callback, secret, generation/revision, destination, fields, exact statuses, comment switch | `WorkConfigureResult`: matching generation/revision and webhook IDs |
| `list_containers` | `WorkListContainersInput`: optional discovery filters/cursor | `WorkListContainersResult`: containers and next cursor |
| `describe_container` | `WorkDescribeContainerInput`: container ID | `WorkDescribeContainerResult`: current types, fields, statuses |
| `create` | `WorkCreateInput`: owner-selected destination/fields and Review source snapshot | `WorkItemLinkResult`: stable provider identity, display key, URL, title, status, optional version/update time |
| `attach` | `WorkAttachInput`: existing link plus one image or deep-link descriptor | `WorkAttachResult`: optional provider attachment ID |
| `comment` | `WorkCommentInput`: existing link, source comment ID, body, author display name/time | `WorkCommentResult`: exact provider comment ID/time and optional version |
| `set_status` | `WorkSetStatusInput`: existing link, binary Review state, exact provider status ID | `WorkSetStatusResult`: resulting status and optional version/update time |
| `hydrate_event` | `WorkHydrateEventInput`: existing link, owning thread/item, opaque reference | `WorkHydrateEventResult`: complete normalized event |
| `disconnect` | `WorkDisconnectInput`: generation and registered webhook IDs | `WorkDisconnectResult`: removed webhook IDs |

Mutations (`configure`, `create`, `attach`, `comment`, `set_status`, `disconnect`) carry an
`idempotencyKey` and ISO 8601 `occurredAt`. Operations on an existing item carry `linkId`,
`externalItemId`, and `threadId`; create carries the thread identity inside `source`.
Persist operation results per install and operation before acknowledging delivery. Reuse the
original result on retry and reject a key reused for a different logical mutation. `eventId` is
the authenticated request's delivery identity; do not substitute a new transport ID for the
operation's stable idempotency key.

`ReviewThreadSource` contains thread/project/app IDs, nullable build version/number, screen
identity, normalized pin coordinates, the first comment, safe device context, and a Tappify deep
link. Its optional screenshot descriptor carries a short-lived URL, attachment ID, media type,
and file name. The host refreshes download URLs on retries; never use a URL as mutation identity.
Create returns a stable item, the host persists its link, and separate attach calls add the deep
link and screenshot. Retrying an attachment must preserve the item and attachment identities.

## Provision callbacks and verify provider webhooks

`configure` supplies the exact per-install `callbackUrl` and signing `secret`. Store the callback
secret encrypted alongside provider webhook routing state. Register provider webhooks against an
opaque route belonging to that install and destination; return their IDs. Keep the provider's
webhook-verification secret separate from the Tappify callback secret.

Verify the provider signature against the exact raw request bytes before parsing or using the
payload for routing. Follow that provider's signing/replay rules. Parsing and reserializing JSON
before verification changes the signed bytes. The backend remains provider-neutral: signature
formats, provider APIs, normalization, and provider webhook lifecycle belong to the extension.

On callback-secret rotation the host provisions another configure generation. Replace routing
and callback state atomically, invalidate stale references as appropriate, and continue using the
new callback/secret. The old callback signature no longer authenticates against the current host
secret. Never infer a callback URL or copy a shared secret from another install.

Normalize provider signals to `comment.created`, `status.changed`, or `item.deleted` and call
[`sendWorkEvent`](https://docs.tappify.ai/extensions/reference/server-api/Function.sendWorkEvent). The helper validates
the event, serializes it once, signs those UTF-8 bytes with HMAC-SHA-256, and posts to the configured
callback with `x-tappify-signature`. These events use the dedicated work callback, independently
of declared alert/banner/chart webhooks and `sendEvent`.

Every event has `externalItemId`, `providerEventId`, `dedupeKey`, ISO 8601 `occurredAt`, and optional
`providerVersion`. Comments include stable `externalCommentId`, body, author ID/display name,
and creation time. Status events include a full `{ id, label, stage }`. Deletion needs no content.

Keep one canonical dedupe identity per logical provider signal across retries, webhook echoes,
and callback rotation. The host deduplicates by `(installId, dedupeKey)` and provider comment
mapping. The production Trello adapter persists one opaque event reference for each provider action
and reuses it as the dedupe key through durable delivery and hydration. Do not derive identity from
receipt time, callback generation, or a randomly generated retry ID.

For identifier-only webhooks, send a nonempty, short-lived `hydrateRef` in place of normalized
comment/status content. The host resolves the owned link, then calls `hydrate_event` with fresh
vault credentials. Return the complete event with exactly the signed `type`, `externalItemId`,
`providerEventId`, `dedupeKey`, `occurredAt`, and `providerVersion`. Bind the reference to the install,
item, owning thread, generation, and expiry. Hydration does not authorize a different event or
require the extension to retain provider access/refresh tokens for webhook processing.

## Delivery, conflicts, and recovery

The host commits Review changes and durable outbox intent together, then performs provider I/O
from background jobs. Provider downtime does not roll back the original Review action. Per-install
delivery state and bounded retries keep one destination's failure from replaying successful peers.
The relay makes one attempt; the durable worker handles retries.

| Failure | Current behavior |
| - | - |
| Network failure, HTTP 429, or 5xx from the extension | Retryable; exhaustion surfaces `failed` |
| HTTP 401/403 | Non-retryable `auth_required`; reconnect the provider |
| HTTP 400/409/422 or malformed successful operation response | Non-retryable `configuration_required`; correct destination/handler configuration |
| Other non-2xx operation response | Non-retryable `failed` |
| Inbound event already applied | Acknowledged as a duplicate without another visible change |
| Inbound event still processing or status timestamp conflict | Retryable callback rejection with `Retry-After: 5`; conflict keeps the link `failed` |

`sendWorkEvent` performs no retry loop. It rejects non-2xx callbacks with `WEBHOOK_REJECTED`,
preserving the HTTP status in `TapServerError`. The adapter must arrange durable redelivery for
transient/network failures, processing conflicts, rate limits, and server errors with the same
event identity. Fix permanent signature, scope, ownership, configuration, or schema rejections
before redelivery. The current intake limit is 60 events per minute per install.

Return the exact provider comment ID and status version from outbound operations so webhook
echoes carry the same identities. The host stores comment mappings and provider status metadata,
suppresses duplicate visible comments, and excludes an inbound change's source link from outbound
fanout. Delivery is at least once; visible inbound comments are deduplicated.

Prefer monotonically increasing numeric or dotted-numeric `providerVersion` strings; the host can
compare these and reject older status versions. Opaque versions are not lexically ordered.
Provider occurrence time is compared with the thread's status observation time; newer observations
win. Equal or unknown observation times that disagree produce `STATUS_TIMESTAMP_CONFLICT`
instead of choosing a side. Provider metadata only advances monotonically; an unrelated comment
does not silently clear a status conflict. There is no general manual-resync operation in this release.

Native work chips show the extension, provider display key, normalized state, and sync health:
`healthy`, `pending`, `auth_required`, `configuration_required`, `failed`, or `external_deleted`.
The drawer offers retry for failed linked delivery, while pending work stays queued. Retry preserves
the existing link and does not call create again. Reconnect or correct configuration for the
corresponding state. A conflict is not guaranteed to clear by pressing retry on an outbound job.

`item.deleted` marks the link `external_deleted`. The host retains its identity, ignores later
comment/status changes for that deleted link, and prevents retry/recreation of the deleted item.
Uninstall attempts `disconnect` while credentials are available; cleanup failure is recorded but
does not prevent revocation. Remove callback secrets, routes, and hydration references. Provider
items need not be deleted; retain idempotency history according to your lifecycle policy and expire
unreachable provider webhooks through provider-owned cleanup.

## Persistence boundaries

Tappify owns durable configuration revisions/generations, Review-to-provider links, comment mappings,
inbound event claims/deduplication, outbox intent, and per-install delivery/retry state. Links are
unique by install/thread and install/provider item. The host draws link chips from these records.

The extension owns provider API behavior, encrypted callback registrations, provider webhook routing,
operation idempotency results, cursors, and hydration references. Use transactional durable storage
for these in a deployed extension. [Hosted extension storage](https://docs.tappify.ai/extensions/build/hosted-storage.md)
can hold settings and UI state, but extension servers cannot write it as a background ledger;
extension storage alone is not the synchronization source of truth.

## Study the production Trello adapter

The production reference lives in `tappify-dev/extensions`, under `apps/trello`. Start with its
`README.md`, then read `server/work.ts`, `server/webhook/handler.ts`,
`server/webhook/normalize.ts`, and `server/delivery.ts`.

1. `list_containers` and `describe_container` discover accessible Trello boards and active lists.
   Configuration records one board, distinct open and resolved lists, and whether comments sync.
2. `create`, `attach`, `comment`, and `set_status` use durable idempotency records. Ambiguous
   provider responses reconcile through stable markers and provider identities instead of
   repeating a visible mutation.
3. Trello signs the untouched request body plus the exact registered callback URL. The adapter
   verifies that signature before parsing, normalizes supported actions, and commits an opaque
   hydration reference before attempting delivery.
4. Durable delivery retries the same event identity. `hydrate_event` fetches current provider
   content with fresh install credentials and returns the complete comment or status event.
5. Configure generations and revisions fence stale routes. Disconnect deletes the known Trello
   webhook before removing its route and synchronization records.

For an isolated sender example, inject a transport that accepts the request in memory. The URL,
secret, and IDs below are fixtures; this code makes no network call and does not exercise host intake.

```ts theme={null}
import { sendWorkEvent } from "@tappify/extension-sdk/server";
import { workCommentCreatedFixture } from "@tappify/extension-sdk/testing";

const event = workCommentCreatedFixture({
  providerEventId: "trello-action-1",
  dedupeKey: "opaque-event-reference-1",
});
const captured: Request[] = [];

await sendWorkEvent(event, {
  callbackUrl: "https://callback.example/work/fake-install",
  secret: "fixture-only-not-a-credential",
  fetch: async (input, init) => {
    captured.push(new Request(input, init));
    return new Response(null, { status: 202 });
  },
});
```

The [testing helpers](https://docs.tappify.ai/extensions/reference/testing-api.md#work-events) also expose
`signedWorkEventRequest` and fixtures for each event type. See [Testing with the SDK](https://docs.tappify.ai/extensions/test/testing-with-the-sdk.md)
and [Local server](https://docs.tappify.ai/extensions/test/local-server.md) for the surrounding development workflow.
The Trello package's focused suites cover work operations, webhook signatures and normalization,
durable delivery, hydration, retries, and disconnect behavior. Its README records the redacted
staging acceptance evidence and remaining provider limitations.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.