pod-content
Create and edit durable Pod pages, business Bases, files, and React applets. Use when the user wants a document saved in the workspace, a shared business table, a data-backed applet, or an independent
SKILL.mdapp/library/skills/pod-content/SKILL.md
GitHubPod content
Chat records the work process. A Page keeps the result. Use the real Pod
contracts through ankole-pod; do not make a local file stand in for a saved
page or use browser storage as the authoritative base.
Run ankole-pod --help before the first operation. It lists the current
operations and parameter shapes. The CLI is available inside an active Agent turn.
Main routes this Skill to a Background Agent Job.
For deterministic recurring data tasks or polling more often than every 30 minutes,
create an Applet Action Job with ankole-pod jobs.create and manage its native
schedule with ankole-pod jobs.schedules.put. Pass business fields in payload; the Action
receives them in ctx.signal.data alongside planned and actual trigger times.
Use ankole-pod jobs.webhooks.create for Job-owned callbacks. These sources need no
Agent wakeup, payload.task, or chat delivery target. Emit a Signal only when
the result needs the Agent's judgment.
Identity and access
The control plane checks your Agent Principal for every request. A personal assistant acts with its human owner's business permissions. An AI colleague uses its own business permissions, including when another Agent delegates work. Human WorkAccess controls admission and ends the work when access is revoked; it does not intersect the colleague's resource permissions with the human's. An Applet Action uses its enabling Human for ordinary capabilities and its Page owner for admin capabilities. It must hold its current execution attempt; review-required and expired attempts cannot access Pod. If access is denied, ask for the required sharing grant. Never use another identity or a hidden storage path.
A page is the unit of permissions and lifecycle. Every page type can have children. A Base holds its tables; tables and records are not pages.
Creating a child requires edit access to its parent page. The child
inherits that access; creation does not add a manage grant. Editing content,
title, or icon requires edit access. Edit access also permits a move when
the page and its descendants keep the same access permissions. A move that
changes those permissions requires Full access (manage) to the source;
every move requires edit access to the destination. Deleting, restoring, or
changing sharing still requires Full access. A rejected move leaves the source
in place. Keep its identity and ask for the needed access rather than claim
a replacement was moved.
Use pages.update_sharing {id, upserts?, removes?} or
ankole-pod page update-sharing <page-id> '<JSON body>' to add, update, or
remove individual grants without replacing other collaborators. A personal
assistant may grant the access implied by its owner's request to collaborate
with a named colleague. Use contribute (comment access) for document reviews and discussion, so the
colleague can answer follow-up comments on the page; use view for read-only
retrieval and edit for edits. Use target_scope: "self" for a single page. Keep unrelated grants. If the owner
cannot manage sharing, explain that a resource manager must grant access.
Read pages.sharing {id} for the current sharing_version, grants, and subjects.
Use pages.set_sharing {id, sharing_version, inherit_permissions?, grants?} or
ankole-pod page set-sharing <page-id> '<JSON body>' to replace sharing.
Each grant supplies one subject (principal_uid, group_id, or source_page_id
with source_action) and either role or exact actions, plus optional
target_scope and condition. A stale version requires a fresh read.
Documents and files
Set a Page's title with pages.create or pages.update;
pages.write supplies its Markdown body. Follow the native Pod delivery
rules for Page authoring and the requested output format.
Every pages.update request that supplies title requires its last-read title as
expected_title, including "" for an untitled Page. The server rejects a missing
or stale base with title_conflict. Read the current title and reconsider the
rename before retrying. Updates to other metadata do not require a title base.
- Use
pages.search {query}to find titles and document text, or read a known page ID. Search returns at most 30 matches. Usepages.children {parent_id}to browse a page's children; omitparent_idfor roots. Passnext_cursorback ascursoruntil it is null.pages.listprovides workspace lists. - Read its Markdown and revision before a change.
- Use
pages.writefor a target body,pages.appendfor Markdown at the end, orpages.patchfor exactold_textandnew_textedits. Each operation requires thatbase_revision. A patch rejects missing or ambiguous text;replace_all: truepermits all matches. The Worker uses the editor's codec. The control plane commits minimal structural and UTF-16 text changes. - If the write conflicts, read again and reconsider the change. Do not overwrite a newer revision or claim the old write succeeded.
Create child content where it belongs: pass the intended document's
parent_id to pages.create, then add a reference to it. Use
<mention page="<document-page-id>" /> for a child document and an embed for
an interactive Base, File Page, or Applet. Do not create elsewhere and move it
merely to create a child. If inserting the reference fails, the child remains
in the page tree. Removing a reference from Markdown removes only the reference.
The page link form is ankole:page/<page-id>.
In document Markdown, put each inline embed on its own line. For an Applet,
use <embed ref="<applet-page-id>" />. For a Base view (including grid,
kanban, gallery, and chart views), use
<embed ref="<base-page-id>" table="<table-id>" view="<view-id>" /> for a
shared saved view, or pass an inline JSON ViewConfig in view when a chart
or filtered view belongs exclusively to this report document. Use the saved page
and table IDs; read saved view IDs and field UUIDs from base.schema. Use
```mermaid for procedural flowcharts, sequences, state machines, and ER
diagrams (supported in both doc and knowledge pages), and ```infographic
for structured narrative roadmaps, SWOT matrices, and comparisons in doc
pages (see the visualization Skill). A Markdown page link opens the referenced
page; an embed renders it inline.
Use files.upload for a file page, document image, or Base attachment.
Image uploads return an ankole:asset/<digest>/<name> address for the document.
Use ankole-files get pages/<page-id>/<path> --pod to read a page file into
your workspace. Uploads and publication commit through the page owner; do not
write arbitrary paths on the Pod drive.
Deleting a page moves its subtree into the trash. It can be restored before purge begins. Deleting a chat does not delete the pages saved from it.
Business data
A primary text field accepts at most 2 KiB of UTF-8 text. A link cell accepts at most 2,000 IDs. Use a one-way child-to-parent link for a parent with more than 2,000 children. The same API serves inline and physical tables.
Create the Base and typed fields before the applet. A new Base has
Table 1 with a primary Name field; read it with base.tables before
creating another table. Give every table and field you create a short
description of at most 2 KiB that states what its values mean: the unit,
currency, tax inclusion, and source system when they apply, such as
Order total in CNY, tax included, imported from the ERP. People and later
Agents read these descriptions in base.schema to interpret the data, so
correct them with base.configure_table or base.update_field when that
meaning changes. Keep the IDs
returned by these operations and continue work on them. A rejected query does
not remove already saved pages or tables. Read the CLI parameter shapes and
correct that query instead of creating replacement content. Records use field
IDs as keys. Query a table and use the returned field versions when you change
records:
- An update sends only the changed data keys and their
expected_versions. An unwritten field has no entry in that map. - A delete sends the record's whole versions map.
- One batch contains at most 200 records and commits as a unit.
- A conflict requires a new read and a new decision. Different fields of the same record can be changed independently.
Contributors can create, update, and delete records and upload record attachments. Editors can change fields, views, and documents and publish Applets. Managers can delete tables and change sharing. A table referenced by a link field must have that reference removed before deletion.
Formula fields are read-only and are evaluated by the server. Reference
same-record fields by name in formula source, such as [Price] * [Quantity].
Do not implement a second formula evaluator in the applet. Linked records
must belong to the same Base.
For a report that must retain today's data, use base.copy with the
source table, target document, and optional filter. The copy has new IDs and
is independent. Links become text with their current titles in link order;
empty links become null. Formulas are checked against the copied field types.
Valid formulas recalculate from copied fields. A formula that cannot use the
new types becomes an ordinary field with its current result; array results
become JSON text. A live dashboard references the source Base instead.
React applets
Use one CLI for initialization, build, publication, and source pull:
ankole-pod applet init <directory>
ankole-pod applet build <directory>
ankole-pod applet publish <directory> '{"id":"<applet-page-id>","base_revision":0}'
ankole-pod applet pull <empty-directory> '{"id":"<applet-page-id>"}'
The generated entry exports provider = defineApplet(App) from
@ankole/applet-sdk. Keep source under src/. Build emits browser ESM,
dist/index.html, dist/main.js, shared chunks, and CSS. React, React DOM, @ankole/uikit,
and @ankole/applet-sdk are host dependencies. Keep all four out of
package.json; the host supplies them. Do not bundle another React
or add a global CSS reset. Keep custom CSS inside the Applet frame.
Import UIKit components from its root export. The build rejects UIKit component
subpaths and stylesheet imports; the host already loads those styles.
Import local images, fonts, and media from source, or reference them with CSS
url(). The build embeds imported resources in the published module graph. A plain
runtime path string does not include a resource in the build. Import a Worker
URL with import workerURL from './calculation.worker.ts?url', then use
new Worker(new URL(workerURL, import.meta.url), { type: 'module' }).
The build compiles the Worker and resolves its URL from nested modules and
dynamic chunks. Applet init supplies the Worker URL import types.
Source pull requires edit access,
checks each file digest, and runs bun install --ignore-scripts.
Pod UI and theme integration
An Applet fills the whole body of a Pod Page sheet (var(--background)). The
Page header above the iframe already shows the breadcrumbs ending in the Page
title and its source conversation, the Automation control, and the more-actions
menu; the iframe starts right below that header and spans the sheet's full width
and remaining height. The host pads the Applet's html with the Page's gutters:
40px on top and 48px on each side (28px on top, 58px and 24px at the
sides on phones), widening the side gutters to center content in a 1440px
column. That space scrolls with the content, so the Applet's scrollbar sits at
the sheet's edge:
- Flush workspace layout: Keep
bodyand.ankole-appletflush with the host surface (padding-inline: 0, transparent orvar(--background)canvas), and leave the host's padding onhtmlin place. Size full-width rows with percentages orwidth: auto, not100vw, because the frame's viewport includes the gutters. Do not wrap the entire Applet in an outer card/border/shadow, or paint a standalone SaaS hero banner or full-bleed background wash. Align content to the gutters' left and right edges. - Page title: The Page body has no title of its own, so an Applet may open
with one
<h1>that rendersusePod().titlein the Pod Page title style (32px, weight600, line height42px). The title follows renames, so never hardcode the Page name. Skip the heading whenusePod().embeddedis true, because the document already shows the title above the embed. Below the heading, or without it, open with compact workspace controls—a toolbar, search/filter bar,Tabs, KPI strip, or primary data view. - Embedded in documents: A document gives an embedded Applet its content's
height, up to one workspace viewport, so the Applet scrolls with the document.
Let content set its own height. A layout sized from the viewport (
100vh) keeps the whole viewport, and an open UIKitDialog,Sheet, orDrawerexpands the embed to the viewport while it is open. - Host-synced light and dark mode: The Pod host sets
html[data-theme="light"]orhtml[data-theme="dark"]and matchingcolor-schemeon the iframe root before first paint, updating both live when the user toggles the external Pod theme.usePod().theme(from@ankole/applet-sdk) anduseTheme().resolvedTheme(from@ankole/uikit) both return'light' | 'dark'reactively and re-render on theme switches. Never hardcode light or dark hex/RGB colors (#fff,#f8fafc,#0f172a,#1e293b), gradients, or@media (prefers-color-scheme: dark)—the user's Pod theme toggle controls[data-theme="dark"]independently of the OS media query. Rely on semantic CSS variables first, use[data-theme="dark"]selectors only when overriding custom CSS rules, and readconst { theme } = usePod()(theme === 'dark') when passing colors to<canvas>, SVG charts, or JavaScript visualizations. - Standard CSS + semantic tokens (no arbitrary Tailwind compilation):
ankole-pod applet buildbundles CSS withBun.buildand does not run a Tailwind compiler over Applet source. Arbitrary Tailwind classes written in JSX (grid-cols-4,bg-slate-900,dark:bg-zinc-800,p-8) will not generate styles unless they already exist in the host bundle. Write Applet layout (display: grid,flex,gap, responsive media queries) insrc/style.csswith Ankole's semantic tokens:- Surfaces:
var(--background)(page paper),var(--layer-01)(raised band, table header, or code block),var(--layer-02),var(--layer-03)/var(--muted),var(--layer-hover)/var(--accent),var(--layer-current)(selected nav/tab wash),var(--field)(input slot),var(--control)(outline button),var(--selection). - Ink & typography:
var(--foreground),var(--fg-secondary)/var(--muted-foreground),var(--fg-tertiary),var(--fg-placeholder),var(--fg-disabled),var(--font-family-sans),var(--font-family-mono). Setfont-variant-numeric: tabular-numson counts, metrics, amounts, dates, and IDs. Keep section headings light and restrained (14px600inline headings or16px400section headings). - Borders & radius:
1px solid var(--border)(var(--border-strong)for stronger dividers) andvar(--radius-control)(4px). Keep controls at4pxand cards/panels on paper square or4pxwithout heavy drop shadows;8pxceramic glass belongs exclusively to UIKit overlays. - Brand & status roles:
var(--primary)(#e10059) +var(--primary-foreground)—limit solid<Button>(variant="default") to at most one primary call-to-action per view; usevariant="outline"for standard actions,variant="ghost"for quiet toolbar/row actions, andvariant="destructive"for irreversible deletes. Status tokens provide mark, text, and subtle container fills:--info/--info-foreground/--info-subtle,--success/--success-foreground/--success-subtle,--warning/--warning-foreground/--warning-subtle, and--destructive/--destructive-foreground/--destructive-subtle(plus perceptual ramps--color-{gray,blue,cyan,teal,green,yellow,orange,red,magenta,purple}-{10..100}for categorical charts: steps60–70in light mode,30–50in dark mode).
- Surfaces:
- Use
@ankole/uikitcomponents and@base-ui/reactcomposition: Native HTML<button>,<input>,<select>, and<table>elements do not carry Pod control styling. Import components from root@ankole/uikit:- Actions & navigation:
Button(variant:'default' | 'outline' | 'ghost' | 'destructive' | 'link',size:'default' | 'sm' | 'xs' | 'icon' | 'icon-sm' | 'icon-xs'),ButtonGroup,Tabs/TabsList(variant:'default' | 'object') /TabsTrigger/TabsContent,Toggle,ToggleGroup,Pagination. - Data & feedback:
Table/TableHeader/TableBody/TableRow/TableHead/TableCell,Card(size:'default' | 'sm') /CardHeader/CardTitle/CardDescription/CardAction/CardContent/CardFooter,Badge(variant:'secondary' | 'info' | 'success' | 'warning' | 'destructive' | 'outline' | 'ghost',size:'default' | 'sm'—there is no'default'variant),Alert/AlertTitle/AlertDescription,Empty/EmptyHeader/EmptyTitle/EmptyDescription/EmptyContent,Progress,Skeleton,Spinner,Separator,Avatar,Kbd,Toaster+toast. - Visualization & charts:
AnkoleStatCard,AnkoleSparkline,AnkoleChart(AntV G2 statistical charts),AnkoleFinancialChart(Lightweight Charts v5 candlestick/OHLCV, volume/MACD/RSI sub-panes, markers, yield curves),AnkoleGraph(AntV G6 topology), andAnkoleInfographic(narrative infographics)—all dynamically code-split behind root@ankole/uikitexports (pair withrecordsToFinancialSeries,densifyTimeBuckets, andformatMetricValuefrom@ankole/applet-sdk; see thevisualizationSkill for grammar and directional color rules:var(--chart-up)/var(--chart-down)default to international green-up/red-down, swapping to red-up/green-down viamarketConvention="cn"only for Mainland China A-share equities). - Forms & filters:
Input,InputGroup,Textarea,Select/SelectTrigger(variant:'field' | 'ghost',size:'default' | 'sm') /SelectValue/SelectContent/SelectItem,Combobox,CreatableCombobox,Checkbox,Switch,RadioGroup/RadioGroupItem,Field/FieldLabel/FieldDescription/FieldError,Label. - Overlays:
Dialog/DialogTrigger/DialogContent/DialogHeader/DialogTitle/DialogDescription/DialogFooter,Sheet,Drawer,Popover,DropdownMenu,Tooltip.@ankole/uikitis built on@base-ui/react(not Radix): compose trigger buttons withrender={<Button variant="outline" />}instead ofasChild.
- Actions & navigation:
Use useRecords(tableID, query) for records and its create, update, and
remove methods for writes. Its TanStack Query result holds rows at
result.data?.records; useAggregate holds groups at result.data?.groups.
usePod().actions lists the selected user's permitted actions on the Applet
page. View access permits use of the Applet. Record writes require contribution
access to the target Base, and file content writes require edit access to
the target Page. Preserve form input when the target rejects a write.
Handle the query's loading and error states before rendering its data.
While a query is pending, show the shape of what is coming instead of plain
loading text. Draw Skeleton blocks shaped like the data the view will
render, such as table rows, cards, or a chart area, and size them in
src/style.css, because the Applet build does not compile Tailwind classes.
Center a Spinner when the content has no predictable shape, and put a small
Spinner beside the text of a field, a menu, or a busy button. The Spinner
is an inline element, so center it with a flex or grid container rather than
auto margins. Give the
loading container role="status" and an aria-label that names what is
loading.
Retain the record when editing begins. Call
update(record.id, data, expectedVersions(record, data)), importing
expectedVersions from @ankole/applet-sdk; call remove(record.id, record.versions)
for deletion. A later query refresh must not replace this edit snapshot. The SDK
updates the shared query cache and refreshes live data. Use useAggregate for
large table statistics. Use usePod().pod for the other Pod contracts,
including pod.fileURL(target) to build a tab-authenticated display URL from a
persisted pages/<id>/... path, GenerateImageResult['file'], or page-relative
file reference.
Use useChannel(room) for temporary presence, cursors, or drag state. Its
on(event, callback) returns an unsubscribe function; register listeners in an
effect and return that function as the effect's cleanup.
Messages are not saved or replayed. Save durable results in Base. For
many simultaneous votes, append one record per vote and aggregate them;
do not repeatedly overwrite one counter.
The Applet runs as its current user in a same-origin iframe. Its document, modules, and Workers use the tab's proven identity. The frame separates DOM, CSS, and viewport layout; it shares the host's origin and trust boundary. The host closes subscriptions and channels when a publication is replaced. Keep DOM and styles in the frame and do not mutate the parent document.
Before publication, read the applet's current revision. Publication replaces its complete source, distribution, and optional backend in one commit. A conflict requires a new read. Open pages replace the old instance with the new revision.
Applet backend routes
Use an optional src/server/main.ts for server validation, atomic record writes,
file parsing, cross-table summaries, or model calls. Ordinary record CRUD still
uses the frontend Base hooks. Import backend types with import type; the
server entry has no runtime module:
import type { Routes } from '@ankole/applet-sdk/server'
export default {
'POST /orders/:id/summary': async ({ params, body }) => {
const { text } = await AnkoleRuntime.ai.generateText({
model: 'light', prompt: `Summarize order ${params.id}: ${body.note}`
})
return { summary: text }
}
} satisfies Routes
ankole-pod applet build bundles this entry into server/main.js as one
ES module. NPM dependencies are bundled; Node built-ins use node: imports. Publish
validates the default export and returns its registered routes and actions.
The export can be a route table or { routes, actions }; each named Action
declares idempotent, optional concurrency (parallel, serial_fifo, or
serial_coalesce), concurrencyKey(ctx), maxSlices, and handler(ctx). No server
source means no backend: rebuilding and publishing removes the old server.
Routes support GET, POST, PUT, PATCH, and DELETE. A handler receives method,
path, decoded params, flat query (repeated values are arrays), parsed JSON
body, principal, applet: {pageId, revision}, and tenant timezone.
Return JSON for 200, undefined for 204, or throw
new AnkoleRuntime.HttpError(400, 'invalid_order', 'Check the order.') for a 4xx
failure. Other exceptions produce a fixed browser error. Agent calls also receive
bounded diagnostic logs and handler exception details.
The function worker supplies Node built-ins, public fetch, an in-memory
/tmp, merged environment values, and AnkoleRuntime capabilities:
base.schema, base.query, base.aggregate, base.write; file.list, file.read,
file.write; metrics.query, which runs a published Brain metric; ai.generateText, ai.generateImage; web.search, web.fetch; mcp.call; and signals.call/emit/run (mirrored on AnkoleRuntime.admin). Read their exact types in
/repo/libs/applet-sdk/src/server.ts. Declare publication environment defaults and required keys in
applet.env.json (up to 64 KiB): each entry maps a POSIX key to either a string default or { default?, required?, secret?, description? }. Runtime values merge Site -> Space -> applet.env.json defaults -> Applet and appear on process.env.<KEY> in the backend worker only. Never hardcode API keys in applet.env.json; declare { "required": true, "secret": true, "description": "..." } so a human editor or administrator configures the secret in Applet settings -> Variables on the Page top bar or in Space Settings. Only editors can read
source, backend files, and environment metadata, and decrypting stored Applet secrets requires page manage permission; viewers and contributors can
read published frontend assets.
Module top-level code must not call AnkoleRuntime; inspection rejects such calls.
Ordinary capabilities use the route caller or the personal Job's Human
authority. AnkoleRuntime.admin uses the Applet's current owner; service Jobs
have no ordinary caller and use that explicit owner surface. Viewing the Applet
does not grant access to its tables or files. GET handlers
cannot write or call ai.generateImage. The runtime cannot create pages or change Base structure.
Function workers are reused by tenant, page, revision, and code-plus-environment digest. Keep
request-specific state inside the handler. Calls have no cross-call transaction; a later failure does not undo committed writes. Put an atomic batch
in one base.write, and include the field versions captured by the read.
ai.generateText, ai.generateImage, web.search, and web.fetch run under the selected execution Principal (AnkoleRuntime uses the caller or personal Job Human; AnkoleRuntime.admin uses the Applet's current owner_uid). Human callers resolve the space's system model profiles and Agent owners resolve that Agent's model profiles, attributing usage to the executing Principal without spending interactive Agent token quota. For ai.generateText, pass a built-in
model profile such as light (never a raw provider/model ID); the
runtime returns {text, finishReason, usage}, so handle an empty or truncated result
when finishReason is length. ai.generateImage accepts {prompt, images?, mask?, action?, size?, quality?, background?, outputFormat?, outputCompression?, moderation?, inputFidelity?, saveTo?} on mutating routes or Actions, storing the generated image in assets/<sha256>/<name> on the Applet Page by default (preserved across code re-publications) or in saveTo: {pageId, name?} on any Base, Document, File, or Applet Page where the execution Principal holds node_edit. On browser routes, result.url carries the calling tab's authentication_ref for immediate <img src={result.url} /> display; persist result.file (pages/<id>/...) in Base records or JSON files and pass that stored reference to usePod().pod.fileURL(file) when rendering later so stored data never embeds tab credentials. images and mask accept Pod pages/<id>/... references. web.search({query, limit?}) returns {query, results: [{title, url, snippet, position?, publishedAt?, score?}]}; web.fetch({urls, maxChars?}) fetches 1–5 https:// URLs with a shared character budget (default 40_000, max 200_000) and returns {success, results: [{url, title?, text?, truncated?, renderWarning?, error?}]}. Runtime errors preserve their status and code.
For an external operation, use
await AnkoleRuntime.mcp.call(tool, args, {server, connection?, account?, skill?}).
server identifies a Toolkit; provide skill to select an enabled Skill's
remote HTTP server. Use admin.mcp.call for the current Page owner. Follow the
Toolkit's generated input/output schema and present HttpError.details.authorization_url
when personal consent is required. Credentials remain outside Applet code.
A GET route may call only tools whose server lists them with
readOnlyHint: true; the host rejects any other tool with read_only_route
before contacting the server. Put a call to a tool without that annotation in
a POST route or an Action, even when it only reads data.
Action handlers receive business configuration in ctx.job.config and the
trigger envelope in ctx.signal. Keep progress in ctx.state.get/set/delete
or ctx.state.scope(name). Each JSON value is limited to 64 KiB. Use
ctx.remainingMs() to budget work and return
await ctx.continueSlice({delaySeconds, progress}) to continue the same Run;
the next slice uses the latest publication. Module variables and /tmp are not
durable state. A failure preserves its input, timestamps, logs and structured
error in the Run, so reconcile uncertain external writes before retrying.
signals.emit({targetJobId,type,data}) queues a same-Page Job; omit targetJobId
to publish to configured Bindings. A route emits on applet://<page-id>; an
Action emits on job://<job-id>. An Agent wakeup
requires a Binding to its conversation. signals.call({actionKey,data}) invokes
a same-Page Action synchronously in the remaining budget; it cannot continue a
slice. GET routes and nested calls remain read-only.
Frontend components can call useAppletJobs() for published Actions, Job
creation/configuration and Run history. useJobSources(jobId) manages that
Job's schedules and Webhooks; both hooks use the current Applet Page.
useActionRun(runId) follows accepted Run progress, results and errors.
Use the built-in Automation panel when a custom control is unnecessary.
For frontend reads, use the original declaration as the route:
const order = useRoute<{total: number}>('GET /orders/:id', {
params: {id: orderId}, query: {currency: 'CNY'}, enabled: Boolean(orderId),
staleTime: 30_000
})
const save = useRouteMutation('POST /orders/:id/summary', {
onSuccess: () => runtime.queryClient.invalidateQueries({queryKey: runtime.routes.key()})
})
// The user action calls save.mutate({params: {id: orderId}, body: {note}}).
Import hooks and usePod from @ankole/applet-sdk; obtain runtime with
usePod(). Query and mutation options and result objects are TanStack Query's.
useRoute accepts only GET; useRouteMutation accepts only POST, PUT, PATCH, or DELETE.
For direct calls use runtime.routes.call({method, path, query, body, signal}).
Route queries include declaration, params, and query in their cache key and abort
on unmount. Access errors hide cached data; temporary failures retain it. Writes
have no optimistic update or automatic route-query refresh. Invalidate only the
queries affected by a successful write. The host binds calls to the loaded
publication revision. A stale revision reloads the applet; the rejected action is
not retried. Preserve important drafts in page-scoped storage and let the user
review them before submitting again.
For an Agent call use:
ankole-pod applet call '{"id":"<applet-page-id>","method":"GET","path":"/orders/123","query":{"tag":["open","priority"]}}'
The result is {status, body?, logs?}. A handler error is a result with its actual
HTTP status and error envelope; do not treat a successful CLI exit as a successful
business action. Route writes use the active turn's write authorization and are
never retried after a disconnect. Read the saved result before deciding whether
a failed or interrupted action needs another submission.
Verify and deliver
First identify which actions the Human authorized the Agent to perform and when. An interaction reserved for a Human is not an Agent completion criterion. Do not execute that interaction against the real Pod, including with scratch records, temporary writes, rollback, or later cleanup. Do not change live business data solely to make a smoke test pass. An Agent-authored write cannot verify a Human-authored interaction.
For construction and writes that the Agent is already authorized to complete, read back the saved page and published manifest. Trace each authorized action's inputs through the handler's complete write payload to the full saved result. Query the real table and check naming and description fields as well as derived values. Aggregate changes or success messages do not replace this complete result check. A successful build checks the distribution; it does not prove that the applet works in the Pod host.
For a Human-reserved interaction, use only side-effect-free workspace checks of the handler's complete payload, schema, and result handling. Include naming and description fields; report this as a logic check. If the Human host interaction has not occurred, deliver the saved result with that limit. Do not continue the Job solely because that Human outcome is absent.
For rendered checks that the Agent is authorized to perform, use the existing browser Skill. The Worker's browser does not automatically inherit the requesting Human's Pod login. If no authorized Pod browser session is available, state that the host interaction is unverified. A standalone host or stub cannot prove the real SDK, Human permissions, or saved writes. Do not create a replacement browser or session to bypass that boundary. When the real host is unavailable, finish the available build and readback checks, then deliver the saved result with that verification limit. Do not build an alternate host as a substitute for the unavailable Human interaction.
Return a clickable link to the saved result in the final report, using
[page title](ankole:page/<page-id>). State what was verified and any remaining
gap. A local source path or build directory is not the saved Pod result.