---
metadata:
  - name: generator
    content: Diplodoc Platform v5.54.5
  - 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.com.tr/support/tracker/en/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.com.tr/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/tracker-plugin-sdk-react`.

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

```tsx
import { uiApi } from "@weavix/tracker-plugin-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 are passed to the Tracker app 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.com.tr/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/tracker-plugin-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.com.tr/support/tracker/en/plugins/slots/index.md) section.

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

**Launch context** is the data that Tracker 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 Tracker | `useTrackerPluginContext()` or `useTrackerPluginContext('basic')` |
| **`full`**                                 | `"contextLevel": "full"`  | Full slot object (`Issue`, trigger context, etc.) — the Tracker app passes 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 passed by Tracker.

```tsx
import { useEffect, useState } from "react";
import {
    trackerApi,
    useTrackerPluginContext,
} from "@weavix/tracker-plugin-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/tracker-plugin-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.com.tr/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.com.tr/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}

The plugin uses `trackerApi` to access the [Tracker public API](https://yandex.com.tr/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.com.tr/support/tracker/en/api-ref/issues/search-issues.md#pagination) and [relative pagination](https://yandex.com.tr/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/tracker-plugin-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.com.tr/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.com.tr/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.com.tr/support/tracker/en/plugins/storage.md). This is a JSON storage of the platform — 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/tracker-plugin-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.com.tr/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 to the platform via the SDK, and the platform proxy executes the request 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.com.tr/support/tracker/en/plugins/externalApi.md) section.

