---
metadata:
  - name: generator
    content: Diplodoc Platform v5.54.2
  - property: og:type
    content: article
  - property: article:section
    content: Plugin platform questions
  - property: og:title
    content: Examples
  - property: article:tag
    content: Technical instructions
alternate:
  - https://yandex.ru/support/tracker/en/plugins/examples.md
  - https://yandex.ru/support/tracker/ru/plugins/examples.md
  - href: en/plugins/examples.md
    type: text/markdown
    title: Markdown version
  - href: ../llms.txt
    type: text/markdown
    title: llms.txt
---
> **Documentation Index:** Fetch the complete configuration index at https://yandex.ru/support/tracker/en/llms.txt


# Plugin platform questions

## Table of contents {#toc}

- [How to navigate the interface?](#how-to-navigate)
- [I have multiple slots, is that OK?](#several-slots)
- [What is context?](#what-is-context)
- [I have a request with pagination, how do I do this?](#pagination)
- [Where to store plugin settings?](#where-store-settings)
- [How to access external APIs?](#external-api)

### How to navigate the interface? {#how-to-navigate}

The plugin runs in an iframe, so navigation to the Tracker interface is handled via `uiApi.navigate`. In the React package, `uiApi` is re-exported from `@weavix/sdk-react`.

**Programmatic navigation** — for example, opening a queue after import:

```tsx
import { uiApi } from "@weavix/sdk-react";

const openQueue = (queueKey: string) => {
    uiApi.navigate({
        path: `/${queueKey}`,
        options: { newTab: true },
    });
};
```

**Links in markup.** `TrackerPluginProvider` intercepts clicks on `<a href="...">` and elements with `data-href`:

- Relative paths and links to the plugin domain open inside the plugin iframe.
- Links to Tracker and external sites go to the host via `uiApi.navigate` (external ones open in a new tab).

```tsx
// Opens in Tracker (in the current or new tab depending on target)
<a href="/TREK-123">Go to issue</a>
<a href="/TREK-123" target="_blank">Open in a new tab</a>

// Opens inside the plugin (if the path is relative)
<a href="/settings">Plugin settings</a>
```

For more information, see [uiApi.navigate](https://yandex.ru/support/tracker/en/plugins/tools/sdk/core.md#navigate).

### I have multiple slots, is that OK? {#several-slots}

Yes. You can specify multiple slots in `manifest.json` — the plugin will appear at each integration point. Usually, all slots share the same `entrypoint` (`index.html`), and the behavior in the code is differentiated by the `slot` field.

```json
{
    "slots": {
        "tracker": {
            "navigation": [
                {
                    "entrypoint": "index.html",
                    "title": { "ru": "Мой отчет", "en": "My report" }
                }
            ],
            "issue.action": [
                {
                    "entrypoint": "index.html",
                    "title": {
                        "ru": "Действие с задачей",
                        "en": "Issue action"
                    }
                }
            ]
        }
    }
}
```

In the code, narrow it down by `slot`:

```tsx
import { useTrackerPluginContext } from "@weavix/sdk-react";

function App() {
    const { slot, slotContext, theme, language } = useTrackerPluginContext();

    if (slot === "navigation") {
        return <ReportPage theme={theme} language={language} />;
    }

    if (slot === "issue.action") {
        return <IssueAction issue={slotContext} />;
    }

    return null;
}
```

For the list of slots and the context format for each, see the [Slots](https://yandex.ru/support/tracker/en/plugins/slots/index.md) section.

### What is context? {#what-is-context}

**Launch context** is what the host passes to the plugin when it opens. In React, it's available via `useTrackerPluginContext()`:

| Field          | What it is                                                        |
| -------------- | ----------------------------------------------------------------- |
| `theme`        | Tracker theme: `light`, `dark`, `system`, etc.                    |
| `language`     | Interface language: `ru`, `en`                                    |
| `slot`         | Slot the plugin was opened from: `navigation`, `issue.action`, …  |
| `slotContext`  | Slot environment data (format depends on the context level)        |
| `contextLevel` | Level declared in the manifest: `basic` or `full`                 |

**Slot context** (`slotContext`) is data from the page where the plugin was opened. The amount of data is set by the **`contextLevel`** field in the slot configuration in `manifest.json` (a required field). You can set the level separately for each slot.

| Level                                      | In the manifest           | What's in `slotContext`                                                                                                    | How to get it in code                                              |
| ------------------------------------------ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| **`basic`** (default in new plugins)       | `"contextLevel": "basic"` | Only `{ entityId, entityMeta? }` — entity ID and optional metadata from the iframe URL, without a request to the host     | `useTrackerPluginContext()` or `useTrackerPluginContext('basic')` |
| **`full`**                                 | `"contextLevel": "full"`  | Full slot object (`Issue`, trigger context, etc.) — the host provides the data via postMessage                            | `useTrackerPluginContext('full')`                                  |

```json
{
    "slots": {
        "tracker": {
            "issue.action": [
                {
                    "entrypoint": "index.html",
                    "title": { "ru": "Мое действие", "en": "My action" },
                    "contextLevel": "full"
                }
            ]
        }
    }
}
```

**`basic`** — use this when it's enough to know _which_ entity was opened (issue key in `entityId` or `entityMeta`), and you load the fields yourself via `trackerApi`. This ensures a faster plugin startup and less data from the host.

```tsx
import { useEffect, useState } from "react";
import {
    trackerApi,
    useTrackerPluginContext,
} from "@weavix/sdk-react";
import type { Issue } from "@weavix/tracker-api-types";

function IssueHeader() {
    const { slotContext } = useTrackerPluginContext<"issue.action">();
    const [issue, setIssue] = useState<Issue | null>(null);

    useEffect(() => {
        if (!slotContext?.entityId) return;
        trackerApi.v3.get["/issues/{id}"]({
            pathParams: { id: slotContext.entityId },
        }).then(({ data }) => setIssue(data));
    }, [slotContext?.entityId]);

    if (!issue) return null;
    return (
        <p>
            {issue.key}: {issue.summary}
        </p>
    );
}
```

**`full`** — use this when you need the issue fields right away, without a separate request. The manifest must have `"contextLevel": "full"`, otherwise the SDK will throw an error when you call `useTrackerPluginContext('full')`.

```tsx
import { getField, useTrackerPluginContext } from "@weavix/sdk-react";

function IssueHeader() {
    const { slotContext } = useTrackerPluginContext<"issue.action">("full");

    if (!slotContext) return null;

    return (
        <p>
            {slotContext.key}: {getField(slotContext, "summary")}
        </p>
    );
}
```

With `full`, the `slotContext` type matches the [public API](https://yandex.ru/support/tracker/en/api-ref/about-api.md) for the slot, for example:

- `issue.action`, `issue.block`, `issue.tab` → `Issue`
- `issue.comment.action` → comment data
- `trigger.create.action` → queue key
- `navigation` → empty object `{}` (data is still loaded via `trackerApi`)

For the full table of slots, see the [Slots](https://yandex.ru/support/tracker/en/plugins/slots/index.md) section.

Theme and language are needed so that the plugin looks like part of Tracker (`ThemeProvider`, `useLocalizedString`). The `full` level is convenient on the issue page, while `basic` is useful when you only need an ID or are already loading data via the API.

### My request uses pagination, how do I do this? {#pagination}

`trackerApi` proxies the [Tracker public API](https://yandex.ru/support/tracker/en/plugins/publicApi.md). The response comes as `{ data, headers }` — headers are used for pagination.

The pagination type depends on the endpoint. For issue search, `POST /issues/_search`, see [page-based pagination](https://yandex.ru/support/tracker/en/api-ref/issues/search-issues.md#pagination) and [relative pagination](https://yandex.ru/support/tracker/en/api-ref/issues/search-issues.md#relative-pagination).

**Page-based search** (`filter`, `query`, or `keys` in the body) — `perPage` and `page` parameters, `X-Total-Count` and `X-Total-Pages` headers:

```tsx
import { useCallback, useState } from "react";
import { trackerApi } from "@weavix/sdk-react";
import type { Issue } from "@weavix/tracker-api-types";

function IssueList() {
    const [issues, setIssues] = useState<Issue[]>([]);
    const [page, setPage] = useState(1);
    const [totalPages, setTotalPages] = useState(1);
    const perPage = 20;

    const loadPage = useCallback(async (nextPage: number) => {
        const { data, headers } = await trackerApi.v3.post["/issues/_search"]({
            queryParams: { perPage, page: nextPage },
            bodyParams: {
                filter: { assignee: "me", status: "open" },
            },
        });

        setIssues(data);
        setPage(nextPage);
        setTotalPages(Number(headers["x-total-pages"] ?? 1));
    }, []);

    return (
        <>
            <ul>
                {issues.map((issue) => (
                    <li key={issue.id}>{issue.key}</li>
                ))}
            </ul>
            <button disabled={page <= 1} onClick={() => loadPage(page - 1)}>
                Previous
            </button>
            <span>
                {page} / {totalPages}
            </span>
            <button
                disabled={page >= totalPages}
                onClick={() => loadPage(page + 1)}
            >
                Next
            </button>
        </>
    );
}
```

**Queue search** (`queue` in the body) — relative pagination: instead of `page`, pass the `id` from the `Link` header of the previous response:

```tsx
const loadFirst = async () => {
    const { data, headers } = await trackerApi.v3.post["/issues/_search"]({
        queryParams: { perPage: 20 },
        bodyParams: { queue: "TREK" },
    });
    setIssues(data);
    setNextPageId(parseNextId(headers.link)); // id from Link: ...; rel="next"
};

const loadNext = async (pageId: string) => {
    const { data, headers } = await trackerApi.v3.post["/issues/_search"]({
        queryParams: { perPage: 20, id: pageId },
        bodyParams: { queue: "TREK" },
    });
    setIssues((prev) => [...prev, ...data]);
    setNextPageId(parseNextId(headers.link));
};

function parseNextId(linkHeader?: string): string | null {
    if (!linkHeader) return null;
    const match = linkHeader.match(/[?&]id=([^&>]+)/);
    return match?.[1] ?? null;
}
```

For large datasets in `_search`, there's also [scroll](https://yandex.ru/support/tracker/en/api-ref/issues/search-issues.md#scroll) — `scrollType`, `scrollId`, and `scrollToken` parameters in `queryParams` (hints are available in `trackerApi.v3.post['/issues/_search']` autocomplete).

Add the required [permissions](https://yandex.ru/support/tracker/en/plugins/common.md#permissions) to `manifest.json`, such as `tracker:issues:read` for reading issues.

### Where should I store plugin settings? {#where-store-settings}

For organization-level settings (shared by all plugin users in that organization), use [`storageApi.orgShared`](https://yandex.ru/support/tracker/en/plugins/storage.md). This is a JSON storage mediated by the host — data survives reloads and is visible across all tabs.

A typical scenario is a plugin settings form: load the current value, disable editing if the user lacks permissions, and save without an explicit version (the SDK will retry the request on conflicts).

```tsx
import { useCallback, useEffect, useState } from "react";
import {
    PluginActionError,
    storageApi,
    useToaster,
    VERSION_CONFLICT,
} from "@weavix/sdk-react";

type Settings = {
    autoReply: boolean;
    welcomeMessage: string;
};

const DEFAULTS: Settings = { autoReply: false, welcomeMessage: "" };

function SettingsForm() {
    const toaster = useToaster();
    const [settings, setSettings] = useState<Settings>(DEFAULTS);
    const [canWrite, setCanWrite] = useState(false);
    const [saving, setSaving] = useState(false);

    useEffect(() => {
        storageApi.orgShared.get("settings").then((record) => {
            if (!record) {
                // Record doesn't exist yet — we can create it
                setCanWrite(true);
                return;
            }
            setSettings({ ...DEFAULTS, ...(record.data as Settings) });
            setCanWrite(record.canWrite);
        });
    }, []);

    const handleSave = useCallback(async () => {
        setSaving(true);
        try {
            // Don't pass version — the SDK reads the current one and retries the request on conflicts
            const updated = await storageApi.orgShared.patch({
                bucket: "settings",
                data: settings,
            });
            // patch returns the merged result (including fields from parallel write processes)
            setSettings({ ...DEFAULTS, ...(updated.data as Settings) });
            toaster.add({ title: "Settings saved", theme: "success" });
        } catch (e) {
            if (e instanceof PluginActionError && e.code === VERSION_CONFLICT) {
                toaster.add({
                    title: "Failed to save",
                    theme: "danger",
                    content:
                        "Data was changed in a parallel session. Reload the form.",
                });
            } else {
                throw e;
            }
        } finally {
            setSaving(false);
        }
    }, [settings, toaster]);

    if (!canWrite) {
        return <p>Only the plugin administrator can change these settings.</p>;
    }

    return (
        <form
            onSubmit={(e) => {
                e.preventDefault();
                handleSave();
            }}
        >
            <label>
                <input
                    type="checkbox"
                    checked={settings.autoReply}
                    onChange={(e) =>
                        setSettings({
                            ...settings,
                            autoReply: e.target.checked,
                        })
                    }
                />
                Auto-reply to new issues
            </label>
            <textarea
                value={settings.welcomeMessage}
                onChange={(e) =>
                    setSettings({ ...settings, welcomeMessage: e.target.value })
                }
            />
            <button type="submit" disabled={saving}>
                Save
            </button>
        </form>
    );
}
```

If the app maintains the current `version` itself (for example, an edit screen stays open for a long time), pass it explicitly to `patch` — then `VERSION_CONFLICT` is returned immediately, without retries, and you can show the user "data has changed, reload". For more information, see the [Versioning](https://yandex.ru/support/tracker/en/plugins/storage.md#versioning) section.

### How do I call external APIs? {#external-api}

The plugin runs in an iframe with a strict CSP policy — direct `fetch` or `XMLHttpRequest` calls to external services will be blocked by the browser. All HTTP requests to external services **must** go through `hostApi.externalApiCall()`: the plugin passes the request parameters via a bridge, the host executes it on its side, and returns the result. The domains that the plugin accesses must be listed in advance in `manifest.json` under the `permissions.external` section.

For more information about configuring the manifest, authorization, error handling, and all `externalApi*` methods, see the [External APIs](https://yandex.ru/support/tracker/en/plugins/externalApi.md) section.

