Presentations
The Presentation resource — a rendered frame for a display or a display-free Scene, with history filtering, renditions, and the plan-capped history window.
A Presentation is one finished result of an Analysis. It comes
in two kinds, and kind is derived from whether it names a display:
| Kind | displayId | What it is |
|---|---|---|
display | set | A frame for one panel, delivered through that display's queue. |
generated | null | A display-free result — Scene JSON and PNG renditions, no panel involved. |
retrieve()
const presentation = await inklet.presentations.retrieve("presentation_123");retrieve(
presentationId: string,
options?: { format?: "png" | "raw2" | "raw4" },
): Promise<Presentation>format selects a rendition on the legacy image field and only applies to
display Presentations. A format other than the three throws
ConfigurationError. Generated Presentations return every rendition they have,
in renditions.
list()
const page = await inklet.presentations.list({
displayId: display.id,
state: "expired",
limit: 20,
});Prop
Type
Without a displayId, scope defaults to generated, so a bare
presentations.list() returns display-free Presentations and none of the
display ones. Ask for scope: "display" or scope: "all" when you want
frames that went to a panel.
Filtering by display
displayId is how you read one panel's history.
displays.listQueue() covers only what has not been
shown yet; this also returns the published, confirmed, and expired ones.
// Everything this panel has shown or is about to show.
const history = await inklet.presentations.list({ displayId: display.id });
// What it is showing now.
const live = await inklet.presentations.list({
displayId: display.id,
state: "published",
});scope is optional alongside displayId and defaults to display, since that
is the only scope a display filter can mean. scope: "all" narrows to the
display half just the same.
scope: "generated" contradicts a displayId filter and is rejected with
400 and code: "invalid_request". A display id that is not well formed is
rejected the same way; one that simply does not exist, or belongs to someone
else, returns an empty page rather than an error.
The history window
Prop
Type
The plan caps how much display history a list may reach. The backend clamps
instead of refusing: Presentations created before the floor are left out of
items, and paging past it ends the list.
const history = await inklet.presentations.list({ displayId: display.id });
if (history.historyWindowStart) {
const since = new Date(history.historyWindowStart);
console.log(`Showing history since ${since.toLocaleDateString()}`);
}Read it as the floor of what you are showing, not as an error. Nothing is deleted — after an upgrade the same call returns the older rows untouched.
historyWindowStart is reported whenever a floor applies, not only when rows
were actually left out — so it is safe to render as "history starts here" on a
page that happens to be short. It is null when no floor applies: an unlimited
plan, or a read with no display half (scope: "generated"). Only the
display half is capped, so scope: "display" and scope: "all" can report a
floor. displays.listQueue(), displays.current(), and
presentations.retrieve() are unaffected: a Presentation whose id you already
hold stays readable either way. See Plans.
The Presentation type
Prop
Type
States
The two kinds use two vocabularies, because most of the words are facts about a
panel and a generated Presentation has no panel. PresentationState is the
union of both.
A generated Presentation is preparing until its Scene and every rendition
it was created with are durable, then ready:
preparing ──→ ready
└─→ failedA display Presentation reports where its delivery has got to:
preparing ──→ queued ──→ published ──→ confirmed
└─→ expired
└─→ failed| State | Kind | Meaning |
|---|---|---|
preparing | both | The render is still being produced. |
ready | generated | The Scene and its renditions are durable. |
queued | display | Waiting for the display to ask for work. |
published | display | Handed to the display. |
confirmed | display | The display reported it is showing this frame. |
expired | display | Superseded, or replaced on the panel. |
failed | both | Rendering or delivery failed; see failure. |
An expired Presentation can be put back on the panel with
displays.setCurrent() — that is how "go back to
the previous image" works.
title
What the Presentation is called in a display's history. inklet resolves it when
the plan is accepted, in this order: the plan's own title, else the template's
title parameter, else the first input Content's title or an excerpt of its
text, else the Analysis title or the template name. It is null only for
Presentations stored before titles were resolved at acceptance.
Which Contents are behind it
Prop
Type
input means the caller named the Content in the Analysis contentIds;
context means the agent retrieved it from the user's history and used it. The
backend orders them input first, in the order the Analysis named them, then
the context refs. A Content appears at most once.
Renditions and images
Prop
Type
Each rendition has its own lifecycle, independent of the Presentation's
state. preparing means the render has not landed yet — read the
Presentation again until it settles. failed means this one geometry could not
be produced; failure says why, the Scene and sibling renditions are
unaffected, and asking for the same geometry again returns the same rendition
rather than retrying. url and expiresAt are null whenever there is no PNG
to link to, so branch on url, never on state alone.
Prop
Type
Every URL here is temporary — expiresAt says when it stops working.
Download the bytes if you need to keep them; do not store the URL.
image is the legacy single-image view, kept for display Presentations and the
format query. New code should read renditions.
Failures
Prop
Type
retryable tells you whether running the same Analysis again is worth trying.
assetIndex points at the specific asset that caused it, when one did.
if (presentation.state === "failed" && presentation.failure) {
const { code, message, retryable, assetIndex } = presentation.failure;
console.error(`${code}: ${message}`);
if (assetIndex !== null) console.error(`asset ${assetIndex} is at fault`);
if (retryable) await retryLater();
}The same shape appears on Content.failure and Analysis.failure.
Generating without a display
generate() is contents.upload() followed by an
Analysis with an { output } target. It registers no display, publishes no
queue entry, and sends nothing to a panel.
const generation = await inklet.presentations.generate({
intent: "Create a calm, glanceable summary",
assets: [inklet.assets.text("Revenue increased 12% this week.")],
output: { viewport: { width: 360, height: 170 }, formats: ["scene", "png"] },
});
const presentation = await inklet.presentations.waitUntilReady(generation);
console.log(presentation.scene?.data);
console.log(presentation.renditions[0]?.url);Prop
Type
preset and viewport are mutually exclusive, and formats defaults to both.
Viewport dimensions are integers between 1 and 8192. Passing mode: "hardcode"
with a single image instead of assets sends one PNG or JPEG through without
AI.
An { output } target always produces exactly one Presentation, so
waitUntilReady() returns it. It throws NoChangeError only when it is handed
an Analysis that named no contentIds and completed as no_change, and
MultiplePresentationsError — with presentationIds — when it is handed one
that produced several, such as an Analysis pinned to several displays. Use
analyses.wait() for those.
waitUntilReady() waits for the Analysis, which settles once the Scene is
stored; the pixels can still be rendering, so check a rendition's url before
using it.
preset names a size. With neither preset nor viewport, inklet uses
default:
preset | Viewport |
|---|---|
default | 800 × 480 — the panel geometry |
macos-widget-small | 170 × 170 |
macos-widget-medium | 360 × 170 |
macos-widget-large | 360 × 376 |
An unknown preset is refused with code: "invalid_request".
Scene JSON
A generated Presentation carries a versioned Scene alongside its PNG renditions:
import { INKLET_SCENE_MEDIA_TYPE } from "@inklethq/sdk";
// "application/vnd.inklet.scene+json;version=1"Prop
Type
Prop
Type
A v1 renderer must handle text, image, and shape. Element-specific
styling lives in properties, so inklet can add an optional style without a
new SDK release. An element type added later arrives as its own string rather
than failing the Presentation — skip one you cannot draw.
render()
A stored Scene can be rendered again at another size without rerunning AI:
const rendition = await inklet.presentations.render(presentation.id, {
viewport: { width: 720, height: 340 },
});render(
presentationId: string,
input: { preset?: string; viewport?: PresentationViewport; colorMode?: PresentationColorMode },
): Promise<PresentationRendition>The output format is always png. The first call for a new geometry returns
the rendition in state: "preparing" with url: null; the backend
deduplicates on format, viewport, and colour mode, so calling again with the
same input returns that same rendition as it stands instead of rendering
twice. Poll retrieve() until the rendition's url is set:
let rendition = await inklet.presentations.render(presentation.id, {
viewport: { width: 720, height: 340 },
});
while (rendition.state === "preparing") {
await new Promise((resolve) => setTimeout(resolve, 2000));
const latest = await inklet.presentations.retrieve(presentation.id);
rendition = latest.renditions.find((r) => r.id === rendition.id) ?? rendition;
}
if (rendition.url) await download(rendition.url);
else console.error(rendition.failure?.message);Downloading a frame
const presentation = await inklet.presentations.retrieve(id, { format: "png" });
const url = presentation.renditions[0]?.url ?? presentation.image?.url;
if (url) {
const response = await fetch(url);
await writeFile(`${presentation.id}.png`, Buffer.from(await response.arrayBuffer()));
}This is a plain fetch — image URLs are pre-authorized and take no inklet
credentials.