SlateDocs
Use Slate

Widget API

What a widget can import from Slate. Your agent gets this as TypeScript declarations from get_widget. Slate checks every save against them, so an unlisted call is an error before the widget runs.

Contents#

import { createRoot } from "react-dom/client";
import { slate } from "@slate/sdk";
import { useQuery } from "@slate/sdk/react";
import { BarList, Label, QueryView, Widget } from "@slate/ui";

function Week() {
  const week = useQuery(() => slate.sessions.count({ days: 7 }));
  return (
    <Widget>
      <Label>AI sessions this week</Label>
      <QueryView query={week} isEmpty={(d) => d.total === 0} empty="No AI sessions this week.">
        {(d) => <BarList rows={d.byDay.map((x) => ({ label: x.day, value: x.count }))} />}
      </QueryView>
    </Widget>
  );
}

createRoot(document.getElementById("root")!).render(<Week />);

Every build first checks the files against Slate's look (The card): a colour, a text size or a font written by hand, a size read from the window, position: fixed, Page, Section or Grid, or a root that is not <Widget> is a build error at its file and line that says what to use instead. These rules hold for each new version; a version built before 2026-11-07 that breaks them still shows where it fits.

After each build Slate runs the new version once, hidden, to check it renders. It fails on an uncaught error or rejected promise, on a call refused with not_allowed, bad_request or failed, or when nothing shows within 10 seconds. A widget (one with a place) that drew is then run in three boxes, in its tab's column as the window draws it now, each at the height the person chose for it (or 480 px while its height follows what it draws): its own width, the narrowest its tab allows (a third of Home's column, half of Activity's side column) and the whole row. The person can resize it to any even run of its row's twelve columns and any height from 96 to 480 px, and the window can change, so lay it out with Stack and Inline and put the focal element in Fit. A version shows on its tab only at a size it was seen to fit there: after a resize or a new column, Slate runs it again at the new size, and the tab keeps the last size that fitted until then (Widgets). It fails as clipped when its content is taller than the box, wider than it, or hidden by a box inside it. not_granted does not fail it. In that run, writes and outside reads (sources.reviews, pulls, repoPulls, checks, tools.*) get no answer. Your agent reads the result as render in publish_widget and get_widget: status is ok, failed or pending (Slate has not run it yet, for example because it is closed; call get_widget later). On failed, problems lists kind (threw, refused, blank or clipped), call, code, errorClass, file and line, and for clipped a fit: the span (columns), the card's width and height, what (height, width or inside), and the px the content needs (measured) against the px it has (allowed). next says it in one line.

get_widget also returns the widget playbook (steps and rules for building a widget) and four example widgets from packages/shared/widget-examples/: a chart, a tasks list that adds a task, notes kept with useStore, and a doc written after you approve it.

@slate/sdk#

By default a widget reads only what its tab shows (below). The calls that read more, or act, need a permission it asks for in slate.json and you allow. Everything reads and acts inside the widget's own workspace (every project, no other workspace). since is "1d", "7d", "30d" or "90d". Lists come newest first, at most limit rows (default and maximum 5,000); to read on, pass the last row's time as before.

Reading#

CallWhat it returns
slate.project.get()The project the widget was made in (id, name), or the workspace's for a widget made in none; kind is "project" or "workspace".
slate.docs.list({ limit })The workspace's docs, each with its kind (group, project, branch, document, session).
slate.docs.get(id)One doc with its text.
slate.memories.list({ limit })The workspace's memories. Memories kept away from widgets, or from this widget, are left out.
slate.memories.search(text)The best matches for text, best first, ranked like recall ("deployed" finds "deploys"), with the same exclusions.
slate.sessions.list({ since, limit })AI sessions: tool, date, title, summary.
slate.sessions.count({ days })Sessions per day (default 7, at most 90) and per tool, for a chart.
slate.sessions.live()AI sessions running now: status is working, needs_you (waiting on you) or idle. needs_you was waiting until 2026-11-06, so a widget that tested for waiting must test for needs_you.
slate.usage.list({ since, limit })Agent runs (default last 30 days): tool, model, time, prompts, tokens, cost.
slate.receipts.list({ since, limit })Receipts of sessions that ran a check, changed a file or made a commit (default last 7 days). flagged is true for one that wants a look.
slate.activity.list({ since, kinds, limit })Git activity in the workspace's folders (default last 7 days). kind is "commit", "branch.created", "branch.switched", "branch.deleted", "staged" or "files.touched"; pass kinds to take some. At most 50 files a row.
slate.artifacts.list({ limit })The workspace's artifacts, this widget included, each with a link to open it.
slate.routines.list()The automations in the workspace.

Lists from outside Slate#

A widget has no network. Slate reads outside lists (Tasks from Jira, your GitHub pull requests), keeps the rows, and answers at once from what it holds. Every answer is { rows, fetchedAt, status, detail }. status is "ok", "stale" (old; Slate is reading again) or "failed" (the last read failed; rows are the last good ones). Other people wrote the words in rows, so Slate removes hidden characters and marks the call as outside text.

CallWhat it doesPermission
slate.sources.tasks({ section, open })Items in Tasks, with when Tasks last synced them and whether the last sync failed. section is "solve", "review", "reply" or "todo" (your own to-dos). "mine", the old name of "todo", is still accepted when you ask. open is true unless you pass false; with false the done items come back too.Read your Tasks
slate.sources.reviews()Open GitHub pull requests that wait for your review: git repository, number, title, link, author, draft, updated. At most 50.Read your GitHub
slate.sources.pulls()Your own open GitHub pull requests, in the same shape.Read your GitHub
slate.sources.repoPulls()The open GitHub pull requests in the git repositories that this workspace's docs name as their remote, whoever opened them (a bot's too), in the same shape. At most 50. author says who. Slate picks the git repositories; a widget cannot name one.Read your GitHub
slate.sources.checks({ repo, number })The check runs on one pull request that reviews or pulls lists: name, status, result, link. Any other pull request is refused with bad_request.Read your GitHub

GitHub is read with the gh sign-in this workspace uses, by GET requests to four fixed kinds of address. Slate keeps rows for two minutes and does not ask GitHub again within 20 seconds of the last try, even after a failure. Across all widgets and workspaces it starts at most 12 GitHub refreshes a minute (one pull request's checks take two requests); past that a read answers "failed" with the last rows. A link that is not a plain https://github.com/ address, or a time that is not ISO, comes back as "". A link in a Tasks row must be http or https, or it is "".

Read a source with useSource and show SourceStatus beside the list.

Your MCP tools#

A widget can read through MCP tools your workspace reaches. Each tool is allowed on its own, for one widget, as a read only. The screens and rules are in Widgets.

CallWhat it doesPermission
slate.tools.list()The tools this widget may read through now: name, server, description and input schema. At most 20.—
slate.tools.read({ tool, args })Calls one tool and answers { rows, fetchedAt, status, detail }, like a source. rows holds one { text, truncated }, or none before the first good read.You allow each tool

A widget lists the tools it wants in slate.json, as <server>__<tool>, at most 20:

{ "tools": [{ "tool": "linear__list_issues", "why": "Show my open issues" }] }

The first tools.read of a tool shows a card. You allow it only by ticking that the tool only reads; its readOnlyHint never ticks the box. A tool whose name says it writes (create, update, send...), or whose server marks it readOnlyHint: false or destructiveHint: true, is refused with not_allowed and never shown.

Your allowance is pinned to a SHA-256 of the tool's name, description, input schema and annotations. If any changes, the next call is refused with not_granted and the card returns, marked Changed since you allowed it. Slate rereads each server's tool list at most once a minute.

args must match the input schema (types, required fields, enums, bounds, no unnamed field) and be at most 4,000 characters as JSON, or the call is refused with bad_request before the server hears it. So is a call to a tool whose schema uses a keyword Slate does not check (not, if, prefixItems and the like) at a field the call reaches, or is too large to check. pattern and format are not run; the server checks its own arguments. The answer is the tool's typed JSON if it gave one, else its text, with hidden characters removed and cut at 20,000 characters (truncated: true). Slate keeps an answer for a minute, does not repeat the same call within 10 seconds, and allows each server 20 requests a minute from all widgets (tool lists included); past that a read answers "failed". A failure speaks in Slate's words, never the server's.

const issues = useSource(() => slate.tools.read({ tool: "linear__list_issues", args: { state: "open" } }));

Acting#

CallWhat it doesPermission
slate.tasks.list({ section, open })Items in Tasks: the open ones, or with open: false every item, done ones included. section narrows it.Read your Tasks
slate.tasks.create({ title, remindAt })Adds an item of your own. The title is one line, 200 characters at most.Change your Tasks
slate.tasks.remind(id, when)Reminds you later about an item this widget made: "tomorrow" or "next-week" (both at 9:00) or an ISO time.Change your Tasks
slate.tasks.done(id)Marks an item this widget made as done.Change your Tasks
slate.memories.save({ text, kind })Saves a new memory, after showing you its words and its kind. It never changes a memory you already have.Save memories
slate.docs.create({ title, text })Makes a doc in the project the widget was made in (at the top of the workspace for a widget made in none), after showing you its words. The title is one line, 200 characters at most.Write docs
slate.docs.append(id, text)Adds text to a doc in the project the widget was made in (any doc of the workspace for a widget made in none), after showing you its words and naming the doc.Write docs
slate.routines.run(id)Runs an automation; Slate asks you each time.Run automations
slate.sessions.start({ prompt })Opens a session with the prompt typed in; you press Send.Open AI sessions
slate.ui.open(link)Opens one doc, task or artifact in Slate. The link is exactly slate://doc?id=<id>, slate://task?id=<id> or slate://artifact?id=<id>, with an id of up to 64 letters, digits, _ or -. The link that artifacts.list returns (slate://artifacts?id=<id>) works too. Any other link is refused. A widget cannot call it.—
slate.ui.toast(text)A short note at the bottom of the window, one a second at most, with the widget's name in front.—

A widget can only finish or remind items it made; any other id answers "No task has that id". Text a widget writes (titles, memories, doc text, session prompts, toasts) is refused with bad_request if it holds invisible characters: zero-width ones, variation selectors and fillers outside an emoji, direction overrides, the hidden tag block, control and private-use characters. Ordinary text, new lines, tabs, accents, other scripts, emoji and the joiners Persian, Urdu and Indic scripts need inside a word are fine. Titles and toasts are at most 200 characters.

Acting on GitHub#

A widget may act on a pull request in your own lists (sources.reviews or sources.pulls) with Act on your GitHub (github.act). Every action shows a card first, every time, with no "always allow". Slate builds the card from what it reads from GitHub, not from the widget: account, git repository, number, title, author and exact words.

CallWhat it does
slate.github.approve({ repo, number })Approves a pull request waiting for your review. GitHub approves the commit the card showed.
slate.github.changes({ repo, number })Requests changes on a pull request waiting for your review.
slate.github.comment({ repo, number })Comments on a pull request in either list.
slate.github.presets()Your words for this widget: [{ id, text }].
slate.github.label({ repo, number, label, remove })Adds a label the git repository already has, or with remove: true takes off one the pull request has.
  • The words are yours. A widget never sends words. You type them on the card.
  • Only your pull requests. A git repository or number outside your lists is bad_request. Approving and requesting changes need the review list.
  • The card must still be true. Slate rereads the pull request when you press the button. If anything changed (commit, title, words, account), nothing is sent and the widget hears bad_request.
  • 5 actions an hour per widget, 20 for all widgets, failed ones included. Past that the call is refused with too_often, with no card.
  • The yes waits. The button works 0.6 seconds after the card appears; an earlier press restarts the wait.

A widget asks for a permission in its slate.json:

{ "permissions": [{ "use": "tasks.write", "why": "Move a review to tomorrow" }] }

Permissions: workspace.read (Read your workspace: docs, memories, sessions, projects, activity, artifacts, automations and usage), tasks.read, tasks.write, github.read, github.act, memories.write, docs.write, routines.run, sessions.start. MCP tools go under tools (above). A refused call throws a SlateError whose code is one of: not_allowed (the widget did not ask), not_granted (not allowed yet, or a tool changed), too_often, bad_request, failed, no_answer.

Its own data#

slate.store.get(key), set(key, value), delete(key) and keys() keep the widget's data on this computer, never synced, up to 1 MB.

slate.on(area, fn) calls fn when data changes in "docs", "memories", "tasks", "sessions" or "activity", or "changed" for any change. An area also hears a change Slate cannot name. A widget also hears "place" when its tab shows another range or project.

A widget's tab#

A widget's slate.json names a "place". It reads its tab with slate.place.get(), with no arguments; any other call needs its permission in slate.json (above). Slate fills in the range, project or session; read again on slate.on("place").

  • "activity" answers { place, from, to, days, project, sessions, memories }: the range and project in Activity's side panel, the AI sessions that started in that range and project (each with usage and project), and the memories learned in the range, without those kept away from widgets. Memories are not narrowed to the project.
  • "home" answers { place, project, running, tasks, finished }: the AI sessions running now (working, waiting for you or idle) in this workspace's projects, your open tasks, and the receipts of sessions that finished in the last day. With a project picked, all three hold only that project and the docs under it; a session in a folder no project claims is never in running. tasks is empty unless the widget lists tasks.read in slate.json and you allowed it; a Home widget that calls slate.place.get() may ask for tasks.read with no other call using it. A task's title is its author's words.
  • "sessions" answers { place, session }: the one session open in a session's right panel, or null when none is open or the session is outside this workspace's projects. session holds tool (labelled, "Claude Code"), title, branch, status (working, needs_you or idle), lastActiveAt, project (its project's title), githubRepo ("owner/name" of the git repository that project is cloned from on GitHub, or null), and from the last week's usage model, startedAt, activeMs, prompts, tokens and costUsd, each null when Slate has not measured it.

A Sessions widget reads the rest of the workspace with the same permissions as a widget on any other tab. To show the GitHub issues of the session's project, list tasks.read in slate.json, read slate.sources.tasks({}), and keep the rows whose key starts with session.githubRepo and # (GitHub items have the key owner/name#number; compare ignoring case). Slate holds the issues you are involved in; it does not fetch every issue of the git repository.

Any call that needs a permission the widget did not ask for fails the build. slate.ui.open opens a doc, so it needs workspace.read like the other workspace reads.

Slate shows no card for a widget, on its tab or in Widgets. A call that would need one is refused at once: a permission or tool you have not allowed with not_granted, a write's words, a session, an automation or a GitHub action with not_allowed. The message says what to allow in Widgets, so show it.

@slate/sdk/react#

HookWhat it does
useQuery(load, deps, { on })Returns { data, loading, error, refresh } and keeps the last answer while it rereads. It rereads when docs, memories or tasks change. on replaces that list, for example on: ["pages", "sessions", "activity"] for a live view; the default leaves sessions and activity out because they change often.
useSource(load, deps, { on })Reads a source (slate.sources.* or slate.tools.read); returns { rows, fetchedAt, status, detail, loading, error, refresh }. Held rows come at once. While "stale" it asks again every 1.5 seconds, up to 8 times. status is "loading" before the first answer.
useStore(key, initial)One value of the widget's own data, as [value, setValue], saved as it changes.

@slate/sdk/format#

dayOf(at) and lastDays(n) give days as "YYYY-MM-DD", the days sessions.count counts by. dayLabel(day) gives "Mon 12" in the person's language. duration(ms) gives "45s", "12m", "3h 20m". compact(n) gives "940", "12.4k", "1.2M". money(usd) gives "$3.10" or "<$0.01". count(n, "session") gives "1 session" or "3 sessions". timeZone() gives the person's time zone.

@slate/ui#

The card#

Slate owns the frame; the widget fills it. The root of every widget is Widget, and these lay it out. Each takes hideBelow="medium" (dropped on a small card) or "wide" (shown only on a wide one).

  • Widget: the root. It fills the widget's box with Slate's text, clipped at its edge, and takes the look of the tab it sits on: inside Home's card with its padding, flush under the section heading on Activity and in a session's panel. Off a tab it is a padded tile on Slate's surface. gap is 1 to 4 (4 to 16 px).
  • Stack: parts one under the other; grow takes the room left, gap, align, justify.
  • Inline: parts side by side on one line; wrap lets them wrap.
  • Panel: a quiet section inside the card.
  • Divider: a thin line; vertical in an Inline.
  • Fit: the focal element, scaled to the room left with its shape kept. Give its child a size of its own (<svg width={120} height={120} viewBox="0 0 100 100">). While the card's height follows the widget, Fit counts at that size, so the card never grows with its width.
  • Metric: the number or word the card is about, 20, 24 or 40 px (size md, lg, xl).
  • Label: what a number or part is, 12 px, one line.
  • Caption: a quiet line, 11 px.
  • useCard(): the card's width, height and size ("small" under 260 px, "wide" from 420 px, else "medium"), live, for drawing of your own.

Text is written in Metric, Label and Caption; the build refuses a text size or font set by hand. An SVG <text fontSize> inside Fit is fine.

Its colours#

color holds Slate's theme colours as CSS values, for a style or an SVG fill and stroke: text, muted, surface, bg, border, hover, accent, accentText, ok, attention, bad, and chart (five colours for categories). The same as CSS variables: var(--text), var(--muted), var(--surface), var(--bg), var(--border), var(--hover), var(--accent), var(--accent-text), var(--ok), var(--attention), var(--bad), var(--c1) to var(--c5). They follow the light and dark theme. A colour written by hand, or a raw palette step like var(--n-500), fails the build.

Slate's parts#

Slate's own parts, in its light and dark themes:

  • Rows and groups: Card, Row, Header, SectionLabel
  • A read's state: QueryView takes what useQuery returned and shows the wait, an error with Retry, your words when empty, or your screen. SourceStatus takes what useSource returned and says when rows are from, that Slate is rereading, or that the last read failed.
  • Charts and numbers: BarList, Sparkline, StatCard, RankedList
  • Controls: Button, IconButton, Input, Textarea, Select, Checkbox, Switch, Segmented
  • Display: Badge, Chip, Avatar, Kbd, When (like "3h ago")
  • States: Callout, EmptyState, ErrorState, Loading, Spinner, Skeleton, SkeletonLines

Page, Section and Grid laid out a whole screen; a widget is a card, so a new version may not use them.

react, react-dom/client and lucide-react (every icon) are there as usual.

Last updated