# Remember data between runs

Build a stateful Block with independent saved memory, realistic tests, and reversible reset.

## When to use memory
Use saved memory for small counters or preferences that belong to one placement. Use [project context](/docs/build/context) for scripts, scenes, shots, assets, and production history. Never put provider keys or media bytes into Block memory.

## Try the complete example
[Download Numbered takes](/block-sdk/examples/numbered-takes.stillmade.json). Import it, review its tests and preview, then confirm the import. Add it to a Project Type and run it twice: the labels advance from Take 1 to Take 2. A second placement has its own counter.

```json
{
  "manifest": {
    "schemaVersion": 1,
    "sdkVersion": "0.1.0",
    "id": "example.numbered-takes",
    "version": "1.0.0",
    "name": "Numbered takes",
    "description": "Label successive creative takes with an independent counter for each production placement.",
    "kind": "task",
    "runtime": "javascript",
    "license": "MIT",
    "provenance": {
      "notice": "MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"
    },
    "inputs": {
      "text": {
        "type": "text",
        "primary": true,
        "semantic": "draft_text",
        "required": true,
        "description": "Creative take text to label with the next number."
      }
    },
    "outputs": {
      "text": {
        "type": "text",
        "primary": true,
        "semantic": "numbered_take",
        "description": "The supplied take prefixed with its saved sequence number."
      }
    },
    "permissions": {
      "project": [
        "state.step"
      ]
    },
    "state": {
      "scope": "step",
      "version": 1,
      "initial": {
        "count": 0
      }
    },
    "ui": [
      {
        "port": "text",
        "control": "text"
      }
    ]
  },
  "code": "state.count += 1; return {text: \"Take \" + state.count + \": \" + input.text.trim()};",
  "tests": [
    {
      "name": "First take",
      "input": {
        "text": "  Opening scene  "
      },
      "expected": {
        "text": "Take 1: Opening scene"
      },
      "expectedState": {
        "version": 1,
        "value": {
          "count": 1
        }
      }
    },
    {
      "name": "Continues saved numbering",
      "input": {
        "text": "Alternate ending"
      },
      "state": {
        "version": 1,
        "value": {
          "count": 4
        }
      },
      "expected": {
        "text": "Take 5: Alternate ending"
      },
      "expectedState": {
        "version": 1,
        "value": {
          "count": 5
        }
      }
    }
  ]
}
```

## Run from the command line
Create a stateful starter, run its fixtures, and save two separate memory checkpoints:

```sh
node packages/block-cli/cli.js create ./takes stateful
node packages/block-cli/cli.js test ./takes
node packages/block-cli/cli.js preview ./takes --save-state take-1.json
node packages/block-cli/cli.js preview ./takes --state take-1.json --save-state take-2.json
```

Without --state, each preview starts fresh. --save-state writes a new file and refuses to overwrite an existing checkpoint. These files contain your Block memory, are pinned to the exact package digest, and should stay private if their contents are private. The CLI never writes state unless you request it.

## Run with the offline SDK
Extract the SDK and save this script alongside its packages folder. The host chooses when to save returned state. Here both runs use a disposable sandbox while the caller holds the accepted state.

```javascript
import {readFile} from 'node:fs/promises';
import {initialBlockState,runPackageAsync} from './packages/block-sdk/index.js';
const pkg=JSON.parse(await readFile('./examples/numbered-takes.stillmade.json','utf8'));
let memory=initialBlockState(pkg.manifest);
const first=await runPackageAsync(pkg,{text:'Opening'},{state:memory});
memory=first.state; // accept only after your host validates current permissions and inputs
const second=await runPackageAsync(pkg,{text:'Alternate opening'},{state:memory});
console.log(first.outputs.text,second.outputs.text);
```

## State contract and limits
Stateful JavaScript Blocks can opt into a small JSON state object. Production
placements save accepted state with their outputs in the existing project document.
Each placement has independent memory pinned to the exact package digest. Connected
runs retain proposed memory until the user accepts their results; changed memory
invalidates an older result. A failed or cancelled run does not advance memory.

Block previews keep temporary memory across runs; Reset or closing the preview
discards it. Import samples and each Project Type rehearsal placement start fresh.
Fixtures never become project memory. External SDK hosts must supply state explicitly
or execution fails with `STATE_REQUIRED`.

Changing a placement to another build starts fresh memory and archives the previous
placement through workflow history; restoring that exact placement restores its
previous memory. Duplicating a workflow placement starts independent fresh memory.
Stateful single-Block batches process the selection in order using provisional
memory. Choosing a batch result accepts memory through that result; later
previews cannot overwrite it. Changes to inputs, selected source versions, or
starting memory require a new batch. Connected multi-Block batches also carry
independent provisional memory for every placement in selection order. Use completed
results validates the original source selection and each memory transition before
committing the completed prefix together. A failed later transform preserves earlier
results for review without advancing saved memory automatically. Single-Block background automation supports saved memory when shared Canvas editing is enabled. Each completed job commits its outputs and memory in one transaction; a changed workspace or memory causes a retry. Failed, paused or stale workers do not advance memory. Selecting a completed result does not advance memory again. Connected background workflows pin all starting states and commit every placement’s memory together when the complete workflow succeeds. Failed prefixes retain their results without advancing shared memory; resuming requires the same starting memory. Hosted APIs and ComfyUI still use their explicit execution flow. State is bounded JSON, not a media-storage API. The production workspace’s
Block memory controls let users reset a placement and undo that reset until
the next accepted run. Reset keeps previous outputs visible but marks them stale.

Declare this alongside the usual manifest fields:

```json
{
  "runtime": "javascript",
  "permissions": {"project": ["state.step"]},
  "state": {"scope": "step", "version": 1, "initial": {"count": 0}}
}
```

Your JavaScript body receives `input` and, only for stateful Blocks, a mutable
`state` object. Update its properties and return your ordinary output ports:

```js
state.count += input.amount;
return {count: state.count};
```

Do not reassign the `state` parameter. Keep it plain JSON. Each invocation uses a
fresh sandbox and a copy of the supplied state. No storage, network, native API,
or another Block's memory is exposed. State is limited to 64 KiB and a positive
schema version. Only JavaScript currently supports this contract.

A trusted host calls:

```js
const previous = initialBlockState(pkg.manifest); // first run only
const result = await runPackageAsync(pkg, {amount: 2}, {state: previous, signal});
// result.outputs is the normal output map.
// result.state is {version: 1, value: {count: 2}}.
```

The SDK never writes persistence itself. The host must store state under the
account, project, and Step instance; recheck the source, previous state, and write
permission; and save the returned state atomically with accepted results. A failed
or cancelled run must not overwrite prior state. Never share one state object
between placements, projects, accounts, or preview sessions. A mismatched state
version fails with `STATE_VERSION`; a host must not silently reinterpret it.

Every stateful fixture requires `expectedState` in addition to ordinary expected
outputs. Optional `state` supplies a saved starting value; omission uses a fresh
copy of `manifest.state.initial` for that fixture:

```json
{
  "name": "Continue a saved count",
  "input": {"amount": 3},
  "state": {"version": 1, "value": {"count": 4}},
  "expected": {"count": 7},
  "expectedState": {"version": 1, "value": {"count": 7}}
}
```

`initialBlockState`, `validateBlockState`, and `STATE_LIMIT_BYTES` are exported by
the SDK. Both browser workers and Node's disposable QuickJS workers carry this
contract. Fixture success verifies state transformation, not host persistence.

## Troubleshooting
- STATE_REQUIRED: your external host did not supply an initial or accepted state envelope.
- STATE_VERSION: the saved version does not match the manifest; restore the previous build or explicitly reset memory. There is no automatic state migration.
- STATE_LIMIT: keep the JSON state value within 64 KiB.
- STATE_BEHAVIOR: output assertions passed but the fixture's expected state did not match.
- STATE_CHANGED: another edit or accepted run changed the memory; run again against current state.

## Test the actual behavior
Each fixture needs expectedState, even when the expected memory is unchanged. Include a fresh-state case and a continuation case. Fixture state is synthetic and never installed as project memory. Run details are available in the production workspace and preview without copying your content into diagnostic downloads.
