Documentation ziprack/1SDK 0.3.0

Capabilities

Attempts

attempt@1capability contract

On this page

Introduction

An attempt is one participant's run of an applet. It holds three things: a status, a state blob you own, and a segment map. The Rack creates the attempt on first launch, hands it back on every launch after that, and freezes it when you complete it.

Declaring attempt@1 is also the only way an applet becomes version-pinned. That is not a side note, so be sure to read Version Pinning before you ship.

An applet without attempt@1 is an app: there is no run and no completion, and everyone is always on the current version.

The Lifecycle

An attempt moves through a short, one-way lifecycle:

launch → in_progress → …work… → complete() → completed → (relaunch lands on the final screen)

Once your applet has launched, you may read the attempt from the Rack.attempt object:

TypeScript
const launch = await Rack.init({ slug: 'my-applet' });

Rack.attempt.id;          // number | null
Rack.attempt.status;      // 'in_progress' | 'completed' | null
Rack.attempt.state();     // your blob, exactly as last saved
Rack.attempt.segment();   // Record<string, string> | null

Every applet declaring attempt@1 owes the participant this check at boot:

TypeScript
if (Rack.attempt.status === 'completed') {
    return renderFinalScreen();
}

const saved = Rack.attempt.state<State>() ?? initialState();
renderStep(saved.step);

In this example, a returning participant who has already finished lands on the final screen, while everyone else resumes at the step they last reached.

Resume State

state is a JSON object, capped at 64 KB encoded. It is for resuming position, plus anything cheap that makes a return visit feel continuous: a draft in a textarea, a scroll offset, or which cards are flipped.

To save state, you may use the setState method:

TypeScript
void Rack.attempt.setState({ step: 4, draft: textarea.value });

Call it after every transition. You don't need to debounce, since pending state writes are coalesced so only the latest value is sent. You don't need to await it either: the local copy updates immediately, so Rack.attempt.state() reflects your write even before the ack lands.

State is not data capture. An answer the participant committed belongs in a statement. If it exists only in state, it will not appear in any export, and a completed attempt will freeze it out of reach. The test is simple: if someone would want it in a spreadsheet, it is a statement.

Segments

Segments are the dimensions reporting filters and groups by, such as track, cohort or region. To get started, declare them in your manifest:

JSON
"segments": [
    {
        "key": "track",
        "label": "Track",
        "values": [
            { "key": "rd", "label": "R&D" },
            { "key": "commercial", "label": "Commercial" }
        ]
    }
]

Then set them as soon as they are known, using the setSegment method:

TypeScript
await Rack.attempt.setSegment({ track: 'rd' });

An attempt may hold at most 20 segment keys, with string values of up to 100 characters. Reporting filters by the first declared segment, so put the one people will actually slice by first.

Segments also drive activity scoping. An activity declaring "segment": "track" is understood to exist only for participants in that track, which keeps export columns honest.

Preview Runs

A participant gets exactly one attempt per applet. The people who manage an applet, you included, get a second track: they may launch in preview, either resuming their open preview run or starting a fresh one.

For anyone who doesn't manage the applet, a preview launch is ignored rather than refused, so a preview link pasted into a chat never dead-ends a participant on an error page.

A preview run is a real attempt, with the same pinning, the same statements, the same records and the same AI budget. However, it carries a preview flag that only the Rack sets. Your applet cannot tell the difference and should not try.

The Rack keeps preview runs out of reporting and exports by default. The live attempt keeps attempt_number 1 for the lifetime of the applet, and preview runs take the numbers above it. Starting a fresh preview never touches the live run, so you may preview an applet you have already completed for real without disturbing your own record of it.

Preview applies to the whole launch, not just the attempt, so it also covers applets without attempt@1. Statements and db@1 records written during a preview carry their own preview flag, too.

Preview does not partition every capability's storage. Per-applet aggregates, such as ai@1 spend and the storage@1 gallery, count preview activity like any other. Budget accordingly on a Rack where people preview a lot.

Completion

When the participant has finished, confirm their last answer and then call the complete method. You may pass an optional summary:

TypeScript
await Rack.activity.answer('outro.confidence', 5);   // confirm the last answer first
await Rack.attempt.complete({ score: 42 });          // optional summary

complete() is idempotent. Calling it twice is not an error, and the second call returns the same result. After it:

  • setState() and setSegment() are refused with completed.
  • Attempt-scoped records freeze.
  • The Rack shows its own completion chrome, and a relaunch lands on the final screen rather than restarting the experience.

That last point is a contract, not a courtesy: build a final screen worth landing on.

The Rack's completion card is dismissible. It announces the moment once, over the top of whatever you just rendered. "Keep looking around" (or Escape) shrinks it to a corner toast that keeps the way out without covering your screen. Nothing reopens it, and a relaunch of a completed attempt shows no card at all, so the last thing the participant sees is yours rather than the Rack's.

Version Pinning

Requesting attempt@1 implies pinned. Its absence implies rolling.

This is one rule, derived rather than declared, with no manifest field to get wrong.

An attempt stays on the version it started on, so activity ids never shift mid-run and corrupt the export. An app rolls, so every participant is on the current version.

There are a few consequences worth knowing before you ship:

  • An applet that adds attempt@1 in a later version changes its own update semantics. The capability diff flags this prominently, and it requires re-approval.
  • In-progress attempts stay pinned to their starting version after a new one is promoted. Two versions run side by side until the last of those attempts drains.
  • Changing an activity id between versions splits export columns. Changing a label only changes the header.