@usestatemachines/sdk 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/agents/index.js CHANGED
@@ -1,22 +1,22 @@
1
1
  // Generated by scripts/build-agents.mjs from templates/ and examples/quickstart.ts.
2
2
 
3
3
  export const prompt =
4
- "Set up State Machines so this project can run code against a disposable replica of a SaaS app, such as Salesforce. Follow these steps in order.\n\n1. Install the SDK: `npm install @usestatemachines/sdk`. It is ESM only and runs on Node.js, at the versions in its `engines` field, and on Cloudflare Workers, not in browsers.\n2. Check that `STATEMACHINES_API_KEY` is set without printing it: `test -n \"$STATEMACHINES_API_KEY\"`. If it is missing, stop and ask me to set it; the SDK doesn't read `.env` files. In Cloudflare Workers, pass the key as `new StateMachines({ apiKey })`.\n3. Save this quickstart unchanged as `state-quickstart.mts` and run `npx tsx state-quickstart.mts` to confirm the setup. It prints `200` and the Salesforce org's limits:\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATEMACHINES_API_KEY\nconst env = await sm.environments.create({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce');\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.delete();\n}\n```\n\n4. Adapt it to my task. Log `await sm.apps.list()` to find each app's ID, and `await sm.apps.versions(appId)` for the actors and APIs of its versions; don't guess IDs. Create only the apps the task needs; a string such as `'salesforce'` names the app and also becomes its name in the environment. `connect()` acts as the app's default actor, the Salesforce administrator; to act as another user, pass an `actorId` from the version's `actors`. To reuse seeded data, create a snapshot once with `await env.snapshots.create()`, then `sm.environments.create({ snapshotId })`.\n5. Always call `env.delete()` in a `finally`; it deletes the environment and its data. If the start fails, the server deletes the environment itself and `create` throws `EnvironmentFailedError`. Pass your own `idempotencyKey` to `create`; if it throws with no response, call it again with the same input and key.\n6. Never print, log or commit keys or tokens, including `credentials.token` and `credentials.headers`.\n\nRead more: https://docs.usestatemachines.com, `node_modules/@usestatemachines/sdk/README.md`, https://app.usestatemachines.com/llms.txt and https://www.npmjs.com/package/@usestatemachines/sdk\n";
4
+ "Install the State Machines skill in this existing repository so I can use disposable app environments with my coding agent.\n\n1. Read the repository instructions. Download the skill from https://docs.usestatemachines.com/skill.md and install it as `state-machines/SKILL.md` in this agent's supported project-local skills directory. Reuse an identical installed copy. Preserve customizations and ask before replacing a different copy. If persistent skills are unsupported, read it for this session and explain the limitation. If the download fails, report the blocker.\n2. Read the skill. Do not create a new project, install the SDK, write example code, change an integration or create remote resources during skill installation. Keep the repository's existing conventions.\n3. The API key belongs in the project's existing secret configuration as `STATEMACHINES_API_KEY`. Never ask me to paste it into chat, print it or commit it. If I need help configuring it, identify the existing secret loader and explain where to save it without reading or displaying secret values.\n4. Report where the skill was installed and whether the agent needs to reload it. Skill installation alone does not verify an API connection. Ask: \"What would you like to run or test?\" Wait for my answer before making further changes.\n";
5
5
 
6
6
  export const llmsTxt =
7
- "# State Machines\n\n> State Machines runs disposable environments of replica SaaS apps, such as Salesforce, for tests and coding agents. Create, connect to, pause, resume and delete them with the TypeScript SDK `@usestatemachines/sdk`.\n\nInstall with `npm install @usestatemachines/sdk`. It is ESM only and runs on Node.js and Cloudflare Workers, not browsers. `engines` in its `package.json` gives the Node.js versions. TypeScript needs `lib: [\"DOM\"]` or `@types/node`. `new StateMachines()` reads the API key from `STATEMACHINES_API_KEY` and doesn't read `.env` files; in Cloudflare Workers, pass `apiKey` explicitly. Never print the key. The installed `node_modules/@usestatemachines/sdk/README.md` matches your version: read it before relying on memory.\n\n## Concepts\n\n- Environment: one disposable run of one or more apps. `const env = await sm.environments.create({ apps: ['salesforce'] })` creates it and waits until it is `running`. Pass `{ wait: false }` to get it back at once in `starting`. Always call `env.delete()` in a `finally`.\n- Apps: a string names the app, and the app's name in the environment is the same string. Use `{ app: 'salesforce', name: 'crm', settings }` for your own name, for two copies of one app, or for settings. Later calls take the name.\n- Pause and resume: `env.pause()` releases the compute and keeps the data; the time limit stops counting. `env.resume()` boots the apps again on the same data and waits until `running`. A paused environment is deleted after one day.\n- Credentials: `const crm = await env.connect('salesforce')` returns `url`, `headers`, `token` and `fetch`. `crm.fetch(path)` joins `path` to `url`, adds `headers`, and puts `token` in the header of the API whose path matches: `Authorization: Bearer` for Salesforce REST and Bulk 2.0. For Salesforce SOAP, put `crm.token` in the envelope's `SessionHeader/sessionId` yourself. `fetch` never sends your API key, never follows redirects and never retries. `token` and `headers` are secrets.\n- Actor: one of the app's users; requests run with that user's permissions. `connect()` acts as the app's default actor (for Salesforce, the administrator) unless you pass `actorId`. Salesforce actor IDs are its user IDs, such as `005…`. Read them from the `actors` of the environment's version: the entry of `(await sm.apps.versions('salesforce')).data` whose `id` is `env.apps[0].versionId`.\n- Snapshot: the saved data of every app of a running environment, taken at one moment. `await env.snapshots.create({ name: 'seeded' })` waits until it is `ready`. `sm.environments.create({ snapshotId })` creates a new environment from it, in place of `apps`; each app keeps its name and version. A pause or delete during the snapshot fails it with `interrupted`.\n- Recorded requests: `sm.requests.list({ environmentId })` pages the calls made to the environment's apps. `sm.requests.get(id)` adds headers and a preview of each body. `sm.requests.body(id, 'request')` or `'response'` returns the full body as bytes. Recording is best effort.\n- YAML: `parseEnvironmentYaml(text)` turns a YAML document with the fields of `create` into its input.\n- Pagination: every `list` returns one page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`.\n\n## Statuses\n\nEnums are open: handle values you don't recognise.\n\n- Environment `status`: `starting`, `running`, `paused`, `failed`, `deleted`. A `deleted` environment has a `reason` when State Machines deleted it: `expired`, `failed` (it never reached `running`) or `cancelled` (staff ended it). The `reason` is `null` when a caller deleted it. A `failed` environment keeps its data for `resume()` and has `failure`: `{ app, code, message }` with `start_failed`, `resume_failed`, `restart_failed`, `version_unavailable` or `invalid_settings`.\n- Snapshot `status`: `saving`, `ready`, `failed`, `deleted`. A failed snapshot has `failure.code` `interrupted` or `internal_error`.\n- `create` and `resume` throw `EnvironmentFailedError` when the environment fails, `UnexpectedStatusError` when it becomes `paused` or `deleted` for another reason, and `WaitTimeoutError` when `timeoutMs` passes.\n- `snapshots.create` throws `SnapshotFailedError` when the snapshot fails and `UnexpectedStatusError` when it was deleted.\n- `sm.environments.wait(id, { status })` and `sm.snapshots.wait(id, { status })` wait for any status, and throw once it cannot be reached without another call.\n\n## Limits\n\n- Running environments are capped per organization: `(await sm.me()).limits.concurrentEnvironments` counts `starting` and `running`.\n- `timeLimitMinutes` has a default and a maximum: `(await sm.me()).limits.timeLimitMinutes`. It counts `starting` and `running` only; at the end the environment is deleted with `reason: 'expired'`.\n\n## Errors and retries\n\nA failed API call throws `StateMachinesError` with `status`, `code`, `message`, `requestId`, `fields` and `retryAfterMs`. The SDK retries `502`, `503`, `504` and connection errors itself, until a deadline; `create` retries reuse one idempotency key. It retries nothing else.\n\n| Code | Status | What to do |\n| --- | --- | --- |\n| `invalid_request` | 400 | Read `fields`, keyed by the dotted path of each wrong field. |\n| `forbidden` | 403 | The key lacks the permission. |\n| `not_found` | 404 | Check the ID and the workspace. |\n| `invalid_status` | 409 | The status does not allow the call: pause needs `running`; resume needs `paused` or `failed`. |\n| `snapshot_in_progress` | 409 | Wait for the running snapshot, then create another. |\n| `idempotency_key_reused` | 409 | The key was used with a different input. Replay with the exact same input. |\n| `workspace_archived` | 409 | The workspace is archived. |\n| `body_too_large` | 413 | The request body is over the API's size limit. Send less. |\n| `version_unavailable` | 422 | The app has no default version. |\n| `environment_limit_reached` | 429 | Pause or delete an environment, then create or resume again. |\n| `unavailable` | 503 | Retried automatically. If `create` still fails, call it again with the same input and your own `idempotencyKey`. |\n\n## Errors from the app URL\n\n`crm.fetch` returns these as responses with the same JSON envelope, and never retries them.\n\n| Code | Status | What to do |\n| --- | --- | --- |\n| `environment_not_running` | 409 | The environment is paused, failed or deleted; `error.status` names it. Resume a paused or failed environment. |\n| `environment_starting` | 503 | Wait `Retry-After` and call again. |\n| `environment_unavailable` | 503 | The app is restarting. Wait `Retry-After` and call again. |\n| `response_too_large` | 502 | The response is over the size limit. Ask for less, for example with a smaller page. |\n| `capacity_exceeded` | 429 | Too many requests in flight to the app. Send fewer at once. |\n| `unsupported_protocol` | 501 | The app does not serve this API version or protocol. |\n\n## Quickstart\n\nSave it unchanged as `state-quickstart.mts` and run `npx tsx state-quickstart.mts`.\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATEMACHINES_API_KEY\nconst env = await sm.environments.create({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce');\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.delete();\n}\n```\n\n## Docs\n\n- [Documentation](https://docs.usestatemachines.com): guides, concepts and reference ([llms.txt](https://docs.usestatemachines.com/llms.txt))\n- [npm package](https://www.npmjs.com/package/@usestatemachines/sdk): install, README and versions\n- [Agent skill](https://app.usestatemachines.com/skill.md): workflow, rules and troubleshooting for coding agents\n- [OpenAPI document](https://cdn.jsdelivr.net/npm/@usestatemachines/sdk/openapi.json): the HTTP API the SDK calls\n- [State Machines](https://usestatemachines.com): website\n";
7
+ "# State Machines\n\n> State Machines runs disposable environments of replica SaaS apps, such as Salesforce, for tests and coding agents. Create, connect to, and delete them with the TypeScript SDK `@usestatemachines/sdk`.\n\nUse the project's package manager to install or reuse `@usestatemachines/sdk`. It is ESM only and runs on Node.js and Cloudflare Workers, not browsers. `engines` in its `package.json` gives the Node.js versions. TypeScript needs `lib: [\"DOM\"]` or `@types/node`. `new StateMachines()` reads the API key from `STATEMACHINES_API_KEY` and doesn't read `.env` files; in Cloudflare Workers, pass `apiKey` explicitly. Never print the key. The installed `node_modules/@usestatemachines/sdk/README.md` matches your version: read it before relying on memory. Read the bundled `agents/SKILL.md` for the task workflow. Reuse the project's secret loader and check the key in the process that runs the test, not just the current shell. Installing the skill does not require SDK installation, example files or remote resources. Ask what the user wants to run and wait. Install the SDK and verify access when their chosen task needs it. Run the connection check below only if the user asks to verify the connection. Do not choose a business workflow from the repository.\n\n## Concepts\n\n- Environment: one disposable run of one or more apps. `const env = await sm.environments.create({ apps: ['salesforce'] })` creates it and waits until it is `running`. Use `{ wait: false }` to retain the ID before startup. Wait for `running` inside `try`, then delete the owned environment in `finally`, including after a wait failure.\n- Apps: a string names the app, and the app's name in the environment is the same string. Use `{ app: 'salesforce', name: 'crm', settings }` for your own name, for two copies of one app, or for settings. Later calls take the name.\n- Credentials: `const crm = await env.connect('salesforce')` returns `url`, `headers`, `token` and `fetch`. `crm.fetch(path)` joins `path` to `url`, adds `headers`, and puts `token` in the header of the API whose path matches: `Authorization: Bearer` for Salesforce REST and Bulk 2.0. For Salesforce SOAP, put `crm.token` in the envelope's `SessionHeader/sessionId` yourself. `fetch` never sends your API key, never follows redirects and never retries. `token` and `headers` are secrets.\n- Actor: one of the app's users; requests run with that user's permissions. `connect()` acts as the app's default actor (for Salesforce, the administrator) unless you pass `actorId`. Salesforce actor IDs are its user IDs, such as `005…`. Find the app by name in `env.apps`, then page through `sm.apps.versions(appId)` to find its exact `versionId`. Read actor IDs from that version's `actors`.\n- Snapshot: the saved data of every app of a running environment, taken at one moment. `await env.snapshots.create({ name: 'seeded' })` waits until it is `ready`. `sm.environments.create({ snapshotId })` creates a new environment from it, in place of `apps`; each app keeps its name and version. A delete during the snapshot fails it with `interrupted`.\n- Recorded requests: `sm.requests.list({ environmentId })` pages the calls made to the environment's apps. `sm.requests.get(id)` adds headers and a preview of each body. `sm.requests.body(id, 'request')` or `'response'` returns the full body as bytes. Recording is best effort.\n- YAML: `parseEnvironmentYaml(text)` turns a YAML document with the fields of `create` into its input.\n- Pagination: every `list` returns one page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`.\n\n## Statuses\n\nEnums are open: handle values you don't recognise.\n\n- Environment `status`: `starting`, `running`, `failed`, `deleted`. A `deleted` environment has a `reason` when State Machines deleted it: `expired`, `failed` (it never reached `running`) or `cancelled` (staff ended it). The `reason` is `null` when a caller deleted it. A `failed` environment has `failure`: `{ app, code, message }` with `start_failed`, `restart_failed`, `version_unavailable` or `invalid_settings`.\n- Snapshot `status`: `saving`, `ready`, `failed`, `deleted`. A failed snapshot has `failure.code` `interrupted` or `internal_error`.\n- `create` throws `EnvironmentFailedError` when the environment fails, `UnexpectedStatusError` when it becomes `deleted` for another reason, and `WaitTimeoutError` when `timeoutMs` passes.\n- `snapshots.create` throws `SnapshotFailedError` when the snapshot fails and `UnexpectedStatusError` when it was deleted.\n- `sm.environments.wait(id, { status })` and `sm.snapshots.wait(id, { status })` wait for any status, and throw once it cannot be reached without another call.\n\n## Limits\n\nWhen `sm.me().limits` is non-null:\n\n- Running environments are capped per organization: `(await sm.me()).limits.concurrentEnvironments` counts `starting` and `running`.\n- `timeLimitMinutes` has a default and a maximum: `(await sm.me()).limits.timeLimitMinutes`. It counts `starting` and `running` only; at the end the environment is deleted with `reason: 'expired'`.\n\n## Errors and retries\n\nA failed API call throws `StateMachinesError` with `status`, `code`, `message`, `requestId`, `fields` and `retryAfterMs`. The SDK retries `502`, `503`, `504` and connection errors itself, until a deadline; `create` retries reuse one idempotency key. It retries nothing else.\n\n| Code | Status | What to do |\n| --- | --- | --- |\n| `invalid_request` | 400 | Read `fields`, keyed by the dotted path of each wrong field. |\n| `forbidden` | 403 | The key lacks the permission. |\n| `not_found` | 404 | Check the ID and the workspace. |\n| `invalid_status` | 409 | The status does not allow the call. Snapshots need a running environment. |\n| `snapshot_in_progress` | 409 | Wait for the running snapshot, then create another. |\n| `idempotency_key_reused` | 409 | The key was used with a different input. Replay with the exact same input. |\n| `workspace_archived` | 409 | The workspace is archived. |\n| `body_too_large` | 413 | The request body is over the API's size limit. Send less. |\n| `version_unavailable` | 422 | The app has no default version. |\n| `environment_limit_reached` | 429 | Delete only this task’s known leftovers, or ask the user to free capacity. |\n| `unavailable` | 503 | Retried automatically. If `create` still has an unknown outcome, replay the same input with the key retained before the original call. Do not generate a new key. |\n\n## Errors from the app URL\n\n`crm.fetch` returns these as responses with the same JSON envelope, and never retries them. Assert the expected status and result. For writes, read back the stored result. Never blindly retry a write whose outcome is unknown.\n\n| Code | Status | What to do |\n| --- | --- | --- |\n| `environment_not_running` | 409 | The environment is failed or deleted; `error.status` names it. Create another environment. |\n| `environment_starting` | 503 | For reads, wait `Retry-After` and retry. Check stored state before retrying an uncertain write. |\n| `environment_unavailable` | 503 | The app is restarting. For reads, wait `Retry-After`. Check stored state before retrying an uncertain write. |\n| `response_too_large` | 502 | The response is over the size limit. Ask for less, for example with a smaller page. |\n| `unsupported_protocol` | 501 | The app does not serve this API version or protocol. |\n\n## Quickstart\n\nUse the project's TypeScript runner and secret loader. For an empty Node.js project, save as `state-quickstart.mts` and run `npx tsx state-quickstart.mts` with the key available. The check prints the response status and deletes the environment. Only after it prints `200`, report verified setup and the rerun command. Continue only with an explicitly supplied task; otherwise ask what to run next and wait with no setup environment left running.\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATEMACHINES_API_KEY\nconst env = await sm.environments.create(\n { apps: ['salesforce'] },\n { wait: false },\n);\n\ntry {\n await sm.environments.wait(env.id, { status: 'running' });\n const salesforce = await env.connect('salesforce');\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.delete();\n}\n```\n\n## Docs\n\n- [Documentation](https://docs.usestatemachines.com): guides, concepts and reference ([llms.txt](https://docs.usestatemachines.com/llms.txt))\n- [npm package](https://www.npmjs.com/package/@usestatemachines/sdk): install, README and versions\n- [Agent skill](https://docs.usestatemachines.com/skill.md): workflow, rules and troubleshooting for coding agents\n- [OpenAPI document](https://cdn.jsdelivr.net/npm/@usestatemachines/sdk/openapi.json): the HTTP API the SDK calls\n- [State Machines](https://usestatemachines.com): website\n";
8
8
 
9
9
  export const skill =
10
- "---\nname: state-machines\ndescription: Use when an agent or test needs a disposable replica of a SaaS app such as Salesforce, to run integration tests or try API calls against a disposable copy instead of a real org. Covers the @usestatemachines/sdk TypeScript SDK - creating environments, credentials, choosing an actor, snapshots, pausing and resuming, cleanup, and errors such as environment_limit_reached and version_unavailable. Also use when the user mentions State Machines, STATEMACHINES_API_KEY or @usestatemachines/sdk.\n---\n\nRead `node_modules/@usestatemachines/sdk/README.md` before relying on memory; it matches the installed version. If the package is missing, run `npm install @usestatemachines/sdk`.\n\n## Workflow\n\n1. Check that `STATEMACHINES_API_KEY` is set without printing it (`test -n \"$STATEMACHINES_API_KEY\"`). If it isn't, ask the user to set it; the SDK doesn't read `.env` files.\n2. Create the environment with only the apps the task needs: `const env = await sm.environments.create({ apps: ['salesforce'] })`. Log `await sm.apps.list()` for each app's ID; don't guess IDs. `create` waits until the environment is `running`. If the start fails, the server deletes the environment and `create` throws `EnvironmentFailedError` with `err.environment.failure`.\n3. Open a `try` right after `create` returns, and call `await env.delete()` in its `finally`. It deletes the environment and its data.\n4. Get credentials for one app: `const crm = await env.connect('salesforce')`. This acts as the app's default actor, the Salesforce administrator. To act with another user's permissions, pass an `actorId` from the `actors` of the environment's version: the entry of `(await sm.apps.versions('salesforce')).data` whose `id` is `env.apps[0].versionId`. Salesforce actor IDs are its user IDs, such as `005…`, never a role name.\n5. Call the app with `crm.fetch(path)`. A leading `/` means the root of the app's `url`.\n\nThe whole flow, as the package ships it. It is ESM with top-level `await`: save it as `state-quickstart.mts` and run `npx tsx state-quickstart.mts`.\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATEMACHINES_API_KEY\nconst env = await sm.environments.create({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce');\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.delete();\n}\n```\n\n## Rules\n\n- Always delete in `finally`.\n- `env.pause()` releases the compute and keeps the data; it is not cleanup. `env.resume()` waits until `running` again. A paused or failed environment is deleted after one day, and still needs `env.delete()` once you are done.\n- To create without waiting, pass `{ wait: false }`, then `sm.environments.wait(env.id, { status: 'running' })`.\n- The time limit counts `starting` and `running`. `(await sm.me()).limits.timeLimitMinutes` gives its `default` and `maximum`. Set `timeLimitMinutes` in `create` for longer runs.\n- Running environments are capped per organization: `limits.concurrentEnvironments` from `sm.me()`. Delete the ones you no longer need. A paused or failed environment doesn't count until it resumes.\n- Never print, log or commit `STATEMACHINES_API_KEY`, `credentials.token` or `credentials.headers`.\n- Salesforce's SOAP API takes the actor token in the envelope, not a header: put `crm.token` in `SessionHeader/sessionId`. `crm.fetch` then sends only the environment token header.\n- `crm.fetch` never sends your API key, never follows redirects and only reaches URLs under the app's `url`.\n- Runtimes: Node.js, at the versions in the package's `engines` field, and Cloudflare Workers. ESM only. TypeScript needs `lib: [\"DOM\"]` or `@types/node`. Never ship an API key to a browser.\n- In Cloudflare Workers, pass the key explicitly: `new StateMachines({ apiKey: env.STATEMACHINES_API_KEY })`. A start takes longer than one request may last, so create with `{ wait: false }` and read `sm.environments.get(id)` in later requests until it is `running`, or pass a `signal` no shorter than the expected start.\n- Every `list` returns one page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`.\n- To reuse seeded data, `await env.snapshots.create({ name })` once, then `sm.environments.create({ snapshotId })` in place of `apps`.\n- Check errors with `instanceof` (`StateMachinesError`, `EnvironmentFailedError`, `SnapshotFailedError`, `UnexpectedStatusError`, `WaitTimeoutError`), not by `name`.\n\n## Recovering a failed create\n\nThe SDK already retries `502`, `503`, `504` and connection errors with one idempotency key. If you need to replay a create whose outcome is unknown, pass your own key: `sm.environments.create(input, { idempotencyKey })`, and call it again with the same input and key. It returns the environment the first call created, or creates it if that call never arrived. A different input fails with `idempotency_key_reused`.\n\n## Troubleshooting\n\n| Symptom | Cause | Fix |\n| --- | --- | --- |\n| `version_unavailable`, 422 | The app has no default version. | Check the app ID against `sm.apps.list()`. |\n| `environment_limit_reached`, 429 | The organization is at its cap of running environments. | List them with `sm.environments.list({ status: ['starting', 'running'] })`, delete or pause one, then create again. |\n| `invalid_status`, 409 | The status does not allow the call, for example pausing an environment that is `starting`. | Read `sm.environments.get(id)` and act on its `status`. |\n| `invalid_request`, 400 | A wrong field. | Read `err.fields`, keyed by dotted path. |\n| `EnvironmentFailedError` | The environment failed to start or resume. | Read `err.environment.failure`: `{ app, code, message }`. `invalid_settings` means the app refused its settings. |\n| `UnexpectedStatusError` | The environment was paused or deleted while the SDK waited. | Read `err.status` and `err.resource`. |\n| `WaitTimeoutError` | The status was not reached within `timeoutMs`. | Wait again with `sm.environments.wait(id, { status })`, or delete the environment. |\n| `snapshot_in_progress`, 409 | The environment already has a snapshot in `saving`. | `const [running] = (await sm.snapshots.list({ environmentId: env.id, status: ['saving'] })).data`, then `await sm.snapshots.wait(running.id, { status: 'ready' })`, then create another. |\n| `SnapshotFailedError` | The snapshot failed. `interrupted` means the environment was paused or deleted first. | Create another snapshot while the environment is `running`. |\n| `environment_not_running`, 409, from `crm.fetch` | The environment is paused, failed or deleted. `crm.fetch` returns it as a response and never retries. | Read `(await res.json()).error.status`; `env.resume()` a paused or failed environment. |\n| `environment_starting` or `environment_unavailable`, 503, from `crm.fetch` | The app is starting or restarting. | Wait the `Retry-After` seconds and call again. |\n| A vendor client such as jsforce is refused by the gateway | The gateway needs `StateMachines-Environment-Token` on every request. | Prefer `crm.fetch`. If you pass `crm.headers` to another client, send them only to `crm.url` and disable redirects. |\n| `TypeError: Pass apiKey or set STATEMACHINES_API_KEY.` | No key in the environment. | Ask the user to set `STATEMACHINES_API_KEY`; in Workers, pass `apiKey`. |\n\nMore: https://docs.usestatemachines.com, https://app.usestatemachines.com/llms.txt and https://www.npmjs.com/package/@usestatemachines/sdk\n";
10
+ "---\nname: state-machines\ndescription: Use when setting up State Machines or running code, integration tests, or API experiments against disposable SaaS replicas such as Salesforce. Applies to @usestatemachines/sdk and STATEMACHINES_API_KEY, including environment creation, actors, snapshots, cleanup and recovery.\n---\n\n## Start with the user's task\n\nDuring skill installation, read the repository instructions and preserve its existing structure. Do not create a new project, install dependencies, write examples or create remote resources. Report the installed skill location and any reload requirement, then ask \"What would you like to run or test?\" and wait. Skill installation does not verify a connection.\n\nOnce the user chooses a task, inspect the project's package manager, runtime, runner and secret loader. Install or reuse a compatible `@usestatemachines/sdk` only when the task needs it. Read the installed README for API signatures and `package.json` for runtime requirements. The SDK is ESM and runs on Node.js and Cloudflare Workers, not in a browser. Follow the user's task and existing project conventions without requiring a separate onboarding example.\n\n## Check access in the right process\n\nThe SDK reads `STATEMACHINES_API_KEY` from the process environment. It does not load `.env` files. Use the project's existing test command or secret loader. An empty variable in your current shell does not prove that the test process lacks it. Verify presence without displaying the value. In Workers, pass the binding explicitly with `new StateMachines({ apiKey: env.STATEMACHINES_API_KEY })`.\n\nCall `sm.me()` to verify access and workspace scope before creating resources. `me.limits` can be null. If the key is missing, prepare the local test or example and tell the user the exact local file, binding, or launch command to configure and rerun, without asking for the key in chat. Do not call the API until access is available. If access is denied or the workspace differs from the task, report the blocker. Do not change membership or permissions.\n\nNever print, log or commit API keys, `credentials.token`, `credentials.headers`, environment dumps, or secret-file contents. Use synthetic test data.\n\n## Run the user’s chosen task\n\nApply this section after setup, once the user has chosen a task.\n\n- `sm.apps.list()` returns app IDs. Every list returns `{ data, nextCursor }`. Follow cursors until the required entry is found or `nextCursor` is null.\n- Create only the apps the task needs. A string such as `'salesforce'` is both its app ID and its name in the environment. For named apps, later `connect()` calls use the name.\n- `connect(appName)` uses the app's default actor, the Salesforce administrator. For another actor, find the matching app by name in `environment.apps`, read its `versionId`, then find that exact version through `sm.apps.versions(appId)`. Use an actor ID from its `actors`, never a role label, workspace member ID, or the first version in a list.\n- Check the declared APIs and [known differences](https://docs.usestatemachines.com/apps/known-differences.md) for the behavior under test. A replica does not import a customer's org or run arbitrary customer automation.\n- Configure the existing integration's test transport with the returned app URL and credentials. Verify its requests reach that URL. A successful standalone SDK request does not prove the integration uses the replica. If transport configuration needs a broader change, explain it before expanding the task.\n- Check the expected HTTP status and assert the result. For writes, read the stored data back and assert the relevant fields. For negative tests, assert the expected error. Do not weaken an assertion to make unsupported behavior pass.\n\n## Own the full lifecycle\n\nCreate with `{ wait: false }` to retain the accepted environment ID before waiting. Enter `try` immediately, then wait for `running`, connect and run the check inside it. Delete in `finally`, including after a startup failure, timeout or failed assertion. An aborted wait does not cancel server work. Cleanup must use a fresh, un-aborted signal. Preserve both verification and cleanup errors, and report the environment ID if deletion fails.\n\nDelete only environments created for this task. If the organization is at its limit, clean up a known leftover from this task or ask the user to free capacity. Do not delete unrelated environments or a user-supplied reusable snapshot.\n\nThe SDK retries management connection errors and `502`, `503`, `504` with one idempotency key per create call. To recover across separate attempts or a restart, retain the input and an explicit `idempotencyKey` before creation. Replay using the same input, key, workspace and caller. Do not create a replacement with a new key while the first outcome is unknown. See [operation recovery](https://docs.usestatemachines.com/sdk-api/recovery.md).\n\n## Optional connection check\n\nRun this only when the user asks to verify the connection. It proves the connection and cleanup, not an existing integration's behavior. Save it in the project's examples or test location and run it with the existing TypeScript runner and secret loader. In a new Node.js project, `state-quickstart.mts` and `npx tsx state-quickstart.mts` are a fallback. Load the key into that process first.\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATEMACHINES_API_KEY\nconst env = await sm.environments.create(\n { apps: ['salesforce'] },\n { wait: false },\n);\n\ntry {\n await sm.environments.wait(env.id, { status: 'running' });\n const salesforce = await env.connect('salesforce');\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.delete();\n}\n```\n\n## Load details when needed\n\n- **Repeated fixtures or parallel tests:** Read [isolated tests](https://docs.usestatemachines.com/sdk-api/testing.md). Save seeded data with `env.snapshots.create({ name })`, then create each test's environment with `{ snapshotId }`. A snapshot in `saving` cannot be deleted yet. Retain its ID and wait for `ready` or `failed` before cleanup. Delete a reusable snapshot only when the task calls for it.\n- **Another actor, vendor client or SOAP:** Read [credentials](https://docs.usestatemachines.com/environments/credentials.md). `credentials.fetch()` confines requests to the app URL, adds its authentication headers, and never follows redirects. External clients must send the environment headers only to that URL and disable redirects. For Salesforce SOAP, put the actor token in `SessionHeader/sessionId` in the envelope.\n- **Cloudflare Workers:** Pass the key explicitly. Use `{ wait: false }` and keep the environment ID across requests or in the job's state. A request ending does not cancel startup. Follow the worker's background-job lifecycle for polling and cleanup.\n- **Long runs:** Read `sm.me().limits` when non-null. The environment time limit covers starting and running. Use a supported `timeLimitMinutes` when the task needs it.\n\n## Recover without changing the test's meaning\n\nManagement failures throw SDK errors. App HTTP failures are returned as `Response` objects. Use `instanceof` with the SDK error classes. Native `credentials.fetch()` never retries. Read the gateway error code and distinguish it from an app's own error.\n\n| Failure | Next action |\n| --- | --- |\n| `invalid_request` | Read `err.fields` and the installed method signature. |\n| `version_unavailable` | Discover the app ID and available versions. Stop if the required version is unavailable. |\n| `environment_limit_reached` | Clean up only this task's known leftovers, or report the capacity blocker. |\n| `EnvironmentFailedError` | Read `err.environment.failure` and clean up the owned environment. |\n| `WaitTimeoutError` or abort | Use the retained environment ID. If older code waited inside `create`, a non-null `WaitTimeoutError.resource` carries its ID. If neither an ID nor a replay key is recoverable, report unresolved cleanup. Do not guess from a list. Wait again only if the task should continue. Otherwise delete the owned environment. |\n| Unknown create outcome | Replay the retained input and idempotency key. Do not start another operation with a new key. |\n| `invalid_status` | Read the current resource status before choosing the next call. |\n| `snapshot_in_progress` | Find the saving snapshot for this environment and wait for it before creating another. |\n| `SnapshotFailedError` | Report the failure. Retry only if the task still needs a snapshot and the source environment is running. |\n| App `environment_not_running` | Inspect the environment. Do not silently switch the integration to another target. |\n| App or gateway `503` | For a retryable read, honor `Retry-After`. For a write with an uncertain outcome, inspect stored state or report uncertainty before retrying. |\n| Cleanup failure | Preserve the task result, report the owned resource ID and retry deletion with valid access and a fresh signal. Do not report cleanup as successful. |\n\n## Finish with evidence\n\nLeave the smallest rerunnable test or example. Report the behavior checked, command, assertion result, SDK and app versions, selected actor, resource IDs without secrets, and cleanup outcome. Say when execution was blocked or only typechecking ran. A passing replica check establishes the asserted behavior for that version and actor, not full production compatibility.\n\nFind other guides through https://docs.usestatemachines.com/llms.txt. Read focused pages before the full documentation bundle. This dashboard also serves an agent index at https://app.usestatemachines.com/llms.txt.\n";
11
11
 
12
12
  export const quickstart =
13
- "import { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATEMACHINES_API_KEY\nconst env = await sm.environments.create({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce');\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.delete();\n}\n";
13
+ "import { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATEMACHINES_API_KEY\nconst env = await sm.environments.create(\n { apps: ['salesforce'] },\n { wait: false },\n);\n\ntry {\n await sm.environments.wait(env.id, { status: 'running' });\n const salesforce = await env.connect('salesforce');\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.delete();\n}\n";
14
14
 
15
15
  export const templates = Object.freeze({
16
16
  prompt:
17
- "Set up State Machines so this project can run code against a disposable replica of a SaaS app, such as Salesforce. Follow these steps in order.\n\n1. Install the SDK: `npm install @usestatemachines/sdk`. It is ESM only and runs on Node.js, at the versions in its `engines` field, and on Cloudflare Workers, not in browsers.\n2. Check that `STATEMACHINES_API_KEY` is set without printing it: `test -n \"$STATEMACHINES_API_KEY\"`. If it is missing, stop and ask me to set it; the SDK doesn't read `.env` files. In Cloudflare Workers, pass the key as `new StateMachines({ apiKey })`.\n3. Save this quickstart unchanged as `state-quickstart.mts` and run `npx tsx state-quickstart.mts` to confirm the setup. It prints `200` and the Salesforce org's limits:\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATEMACHINES_API_KEY\nconst env = await sm.environments.create({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce');\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.delete();\n}\n```\n\n4. Adapt it to my task. Log `await sm.apps.list()` to find each app's ID, and `await sm.apps.versions(appId)` for the actors and APIs of its versions; don't guess IDs. Create only the apps the task needs; a string such as `'salesforce'` names the app and also becomes its name in the environment. `connect()` acts as the app's default actor, the Salesforce administrator; to act as another user, pass an `actorId` from the version's `actors`. To reuse seeded data, create a snapshot once with `await env.snapshots.create()`, then `sm.environments.create({ snapshotId })`.\n5. Always call `env.delete()` in a `finally`; it deletes the environment and its data. If the start fails, the server deletes the environment itself and `create` throws `EnvironmentFailedError`. Pass your own `idempotencyKey` to `create`; if it throws with no response, call it again with the same input and key.\n6. Never print, log or commit keys or tokens, including `credentials.token` and `credentials.headers`.\n\nRead more: https://docs.usestatemachines.com, `node_modules/@usestatemachines/sdk/README.md`, {{origin}}/llms.txt and https://www.npmjs.com/package/@usestatemachines/sdk\n",
17
+ "Install the State Machines skill in this existing repository so I can use disposable app environments with my coding agent.\n\n1. Read the repository instructions. Download the skill from https://docs.usestatemachines.com/skill.md and install it as `state-machines/SKILL.md` in this agent's supported project-local skills directory. Reuse an identical installed copy. Preserve customizations and ask before replacing a different copy. If persistent skills are unsupported, read it for this session and explain the limitation. If the download fails, report the blocker.\n2. Read the skill. Do not create a new project, install the SDK, write example code, change an integration or create remote resources during skill installation. Keep the repository's existing conventions.\n3. The API key belongs in the project's existing secret configuration as `STATEMACHINES_API_KEY`. Never ask me to paste it into chat, print it or commit it. If I need help configuring it, identify the existing secret loader and explain where to save it without reading or displaying secret values.\n4. Report where the skill was installed and whether the agent needs to reload it. Skill installation alone does not verify an API connection. Ask: \"What would you like to run or test?\" Wait for my answer before making further changes.\n",
18
18
  llmsTxt:
19
- "# State Machines\n\n> State Machines runs disposable environments of replica SaaS apps, such as Salesforce, for tests and coding agents. Create, connect to, pause, resume and delete them with the TypeScript SDK `@usestatemachines/sdk`.\n\nInstall with `npm install @usestatemachines/sdk`. It is ESM only and runs on Node.js and Cloudflare Workers, not browsers. `engines` in its `package.json` gives the Node.js versions. TypeScript needs `lib: [\"DOM\"]` or `@types/node`. `new StateMachines()` reads the API key from `STATEMACHINES_API_KEY` and doesn't read `.env` files; in Cloudflare Workers, pass `apiKey` explicitly. Never print the key. The installed `node_modules/@usestatemachines/sdk/README.md` matches your version: read it before relying on memory.\n\n## Concepts\n\n- Environment: one disposable run of one or more apps. `const env = await sm.environments.create({ apps: ['salesforce'] })` creates it and waits until it is `running`. Pass `{ wait: false }` to get it back at once in `starting`. Always call `env.delete()` in a `finally`.\n- Apps: a string names the app, and the app's name in the environment is the same string. Use `{ app: 'salesforce', name: 'crm', settings }` for your own name, for two copies of one app, or for settings. Later calls take the name.\n- Pause and resume: `env.pause()` releases the compute and keeps the data; the time limit stops counting. `env.resume()` boots the apps again on the same data and waits until `running`. A paused environment is deleted after one day.\n- Credentials: `const crm = await env.connect('salesforce')` returns `url`, `headers`, `token` and `fetch`. `crm.fetch(path)` joins `path` to `url`, adds `headers`, and puts `token` in the header of the API whose path matches: `Authorization: Bearer` for Salesforce REST and Bulk 2.0. For Salesforce SOAP, put `crm.token` in the envelope's `SessionHeader/sessionId` yourself. `fetch` never sends your API key, never follows redirects and never retries. `token` and `headers` are secrets.\n- Actor: one of the app's users; requests run with that user's permissions. `connect()` acts as the app's default actor (for Salesforce, the administrator) unless you pass `actorId`. Salesforce actor IDs are its user IDs, such as `005…`. Read them from the `actors` of the environment's version: the entry of `(await sm.apps.versions('salesforce')).data` whose `id` is `env.apps[0].versionId`.\n- Snapshot: the saved data of every app of a running environment, taken at one moment. `await env.snapshots.create({ name: 'seeded' })` waits until it is `ready`. `sm.environments.create({ snapshotId })` creates a new environment from it, in place of `apps`; each app keeps its name and version. A pause or delete during the snapshot fails it with `interrupted`.\n- Recorded requests: `sm.requests.list({ environmentId })` pages the calls made to the environment's apps. `sm.requests.get(id)` adds headers and a preview of each body. `sm.requests.body(id, 'request')` or `'response'` returns the full body as bytes. Recording is best effort.\n- YAML: `parseEnvironmentYaml(text)` turns a YAML document with the fields of `create` into its input.\n- Pagination: every `list` returns one page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`.\n\n## Statuses\n\nEnums are open: handle values you don't recognise.\n\n- Environment `status`: `starting`, `running`, `paused`, `failed`, `deleted`. A `deleted` environment has a `reason` when State Machines deleted it: `expired`, `failed` (it never reached `running`) or `cancelled` (staff ended it). The `reason` is `null` when a caller deleted it. A `failed` environment keeps its data for `resume()` and has `failure`: `{ app, code, message }` with `start_failed`, `resume_failed`, `restart_failed`, `version_unavailable` or `invalid_settings`.\n- Snapshot `status`: `saving`, `ready`, `failed`, `deleted`. A failed snapshot has `failure.code` `interrupted` or `internal_error`.\n- `create` and `resume` throw `EnvironmentFailedError` when the environment fails, `UnexpectedStatusError` when it becomes `paused` or `deleted` for another reason, and `WaitTimeoutError` when `timeoutMs` passes.\n- `snapshots.create` throws `SnapshotFailedError` when the snapshot fails and `UnexpectedStatusError` when it was deleted.\n- `sm.environments.wait(id, { status })` and `sm.snapshots.wait(id, { status })` wait for any status, and throw once it cannot be reached without another call.\n\n## Limits\n\n- Running environments are capped per organization: `(await sm.me()).limits.concurrentEnvironments` counts `starting` and `running`.\n- `timeLimitMinutes` has a default and a maximum: `(await sm.me()).limits.timeLimitMinutes`. It counts `starting` and `running` only; at the end the environment is deleted with `reason: 'expired'`.\n\n## Errors and retries\n\nA failed API call throws `StateMachinesError` with `status`, `code`, `message`, `requestId`, `fields` and `retryAfterMs`. The SDK retries `502`, `503`, `504` and connection errors itself, until a deadline; `create` retries reuse one idempotency key. It retries nothing else.\n\n| Code | Status | What to do |\n| --- | --- | --- |\n| `invalid_request` | 400 | Read `fields`, keyed by the dotted path of each wrong field. |\n| `forbidden` | 403 | The key lacks the permission. |\n| `not_found` | 404 | Check the ID and the workspace. |\n| `invalid_status` | 409 | The status does not allow the call: pause needs `running`; resume needs `paused` or `failed`. |\n| `snapshot_in_progress` | 409 | Wait for the running snapshot, then create another. |\n| `idempotency_key_reused` | 409 | The key was used with a different input. Replay with the exact same input. |\n| `workspace_archived` | 409 | The workspace is archived. |\n| `body_too_large` | 413 | The request body is over the API's size limit. Send less. |\n| `version_unavailable` | 422 | The app has no default version. |\n| `environment_limit_reached` | 429 | Pause or delete an environment, then create or resume again. |\n| `unavailable` | 503 | Retried automatically. If `create` still fails, call it again with the same input and your own `idempotencyKey`. |\n\n## Errors from the app URL\n\n`crm.fetch` returns these as responses with the same JSON envelope, and never retries them.\n\n| Code | Status | What to do |\n| --- | --- | --- |\n| `environment_not_running` | 409 | The environment is paused, failed or deleted; `error.status` names it. Resume a paused or failed environment. |\n| `environment_starting` | 503 | Wait `Retry-After` and call again. |\n| `environment_unavailable` | 503 | The app is restarting. Wait `Retry-After` and call again. |\n| `response_too_large` | 502 | The response is over the size limit. Ask for less, for example with a smaller page. |\n| `capacity_exceeded` | 429 | Too many requests in flight to the app. Send fewer at once. |\n| `unsupported_protocol` | 501 | The app does not serve this API version or protocol. |\n\n## Quickstart\n\nSave it unchanged as `state-quickstart.mts` and run `npx tsx state-quickstart.mts`.\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATEMACHINES_API_KEY\nconst env = await sm.environments.create({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce');\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.delete();\n}\n```\n\n## Docs\n\n- [Documentation](https://docs.usestatemachines.com): guides, concepts and reference ([llms.txt](https://docs.usestatemachines.com/llms.txt))\n- [npm package](https://www.npmjs.com/package/@usestatemachines/sdk): install, README and versions\n- [Agent skill]({{origin}}/skill.md): workflow, rules and troubleshooting for coding agents\n- [OpenAPI document](https://cdn.jsdelivr.net/npm/@usestatemachines/sdk/openapi.json): the HTTP API the SDK calls\n- [State Machines](https://usestatemachines.com): website\n",
19
+ "# State Machines\n\n> State Machines runs disposable environments of replica SaaS apps, such as Salesforce, for tests and coding agents. Create, connect to, and delete them with the TypeScript SDK `@usestatemachines/sdk`.\n\nUse the project's package manager to install or reuse `@usestatemachines/sdk`. It is ESM only and runs on Node.js and Cloudflare Workers, not browsers. `engines` in its `package.json` gives the Node.js versions. TypeScript needs `lib: [\"DOM\"]` or `@types/node`. `new StateMachines()` reads the API key from `STATEMACHINES_API_KEY` and doesn't read `.env` files; in Cloudflare Workers, pass `apiKey` explicitly. Never print the key. The installed `node_modules/@usestatemachines/sdk/README.md` matches your version: read it before relying on memory. Read the bundled `agents/SKILL.md` for the task workflow. Reuse the project's secret loader and check the key in the process that runs the test, not just the current shell. Installing the skill does not require SDK installation, example files or remote resources. Ask what the user wants to run and wait. Install the SDK and verify access when their chosen task needs it. Run the connection check below only if the user asks to verify the connection. Do not choose a business workflow from the repository.\n\n## Concepts\n\n- Environment: one disposable run of one or more apps. `const env = await sm.environments.create({ apps: ['salesforce'] })` creates it and waits until it is `running`. Use `{ wait: false }` to retain the ID before startup. Wait for `running` inside `try`, then delete the owned environment in `finally`, including after a wait failure.\n- Apps: a string names the app, and the app's name in the environment is the same string. Use `{ app: 'salesforce', name: 'crm', settings }` for your own name, for two copies of one app, or for settings. Later calls take the name.\n- Credentials: `const crm = await env.connect('salesforce')` returns `url`, `headers`, `token` and `fetch`. `crm.fetch(path)` joins `path` to `url`, adds `headers`, and puts `token` in the header of the API whose path matches: `Authorization: Bearer` for Salesforce REST and Bulk 2.0. For Salesforce SOAP, put `crm.token` in the envelope's `SessionHeader/sessionId` yourself. `fetch` never sends your API key, never follows redirects and never retries. `token` and `headers` are secrets.\n- Actor: one of the app's users; requests run with that user's permissions. `connect()` acts as the app's default actor (for Salesforce, the administrator) unless you pass `actorId`. Salesforce actor IDs are its user IDs, such as `005…`. Find the app by name in `env.apps`, then page through `sm.apps.versions(appId)` to find its exact `versionId`. Read actor IDs from that version's `actors`.\n- Snapshot: the saved data of every app of a running environment, taken at one moment. `await env.snapshots.create({ name: 'seeded' })` waits until it is `ready`. `sm.environments.create({ snapshotId })` creates a new environment from it, in place of `apps`; each app keeps its name and version. A delete during the snapshot fails it with `interrupted`.\n- Recorded requests: `sm.requests.list({ environmentId })` pages the calls made to the environment's apps. `sm.requests.get(id)` adds headers and a preview of each body. `sm.requests.body(id, 'request')` or `'response'` returns the full body as bytes. Recording is best effort.\n- YAML: `parseEnvironmentYaml(text)` turns a YAML document with the fields of `create` into its input.\n- Pagination: every `list` returns one page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`.\n\n## Statuses\n\nEnums are open: handle values you don't recognise.\n\n- Environment `status`: `starting`, `running`, `failed`, `deleted`. A `deleted` environment has a `reason` when State Machines deleted it: `expired`, `failed` (it never reached `running`) or `cancelled` (staff ended it). The `reason` is `null` when a caller deleted it. A `failed` environment has `failure`: `{ app, code, message }` with `start_failed`, `restart_failed`, `version_unavailable` or `invalid_settings`.\n- Snapshot `status`: `saving`, `ready`, `failed`, `deleted`. A failed snapshot has `failure.code` `interrupted` or `internal_error`.\n- `create` throws `EnvironmentFailedError` when the environment fails, `UnexpectedStatusError` when it becomes `deleted` for another reason, and `WaitTimeoutError` when `timeoutMs` passes.\n- `snapshots.create` throws `SnapshotFailedError` when the snapshot fails and `UnexpectedStatusError` when it was deleted.\n- `sm.environments.wait(id, { status })` and `sm.snapshots.wait(id, { status })` wait for any status, and throw once it cannot be reached without another call.\n\n## Limits\n\nWhen `sm.me().limits` is non-null:\n\n- Running environments are capped per organization: `(await sm.me()).limits.concurrentEnvironments` counts `starting` and `running`.\n- `timeLimitMinutes` has a default and a maximum: `(await sm.me()).limits.timeLimitMinutes`. It counts `starting` and `running` only; at the end the environment is deleted with `reason: 'expired'`.\n\n## Errors and retries\n\nA failed API call throws `StateMachinesError` with `status`, `code`, `message`, `requestId`, `fields` and `retryAfterMs`. The SDK retries `502`, `503`, `504` and connection errors itself, until a deadline; `create` retries reuse one idempotency key. It retries nothing else.\n\n| Code | Status | What to do |\n| --- | --- | --- |\n| `invalid_request` | 400 | Read `fields`, keyed by the dotted path of each wrong field. |\n| `forbidden` | 403 | The key lacks the permission. |\n| `not_found` | 404 | Check the ID and the workspace. |\n| `invalid_status` | 409 | The status does not allow the call. Snapshots need a running environment. |\n| `snapshot_in_progress` | 409 | Wait for the running snapshot, then create another. |\n| `idempotency_key_reused` | 409 | The key was used with a different input. Replay with the exact same input. |\n| `workspace_archived` | 409 | The workspace is archived. |\n| `body_too_large` | 413 | The request body is over the API's size limit. Send less. |\n| `version_unavailable` | 422 | The app has no default version. |\n| `environment_limit_reached` | 429 | Delete only this task’s known leftovers, or ask the user to free capacity. |\n| `unavailable` | 503 | Retried automatically. If `create` still has an unknown outcome, replay the same input with the key retained before the original call. Do not generate a new key. |\n\n## Errors from the app URL\n\n`crm.fetch` returns these as responses with the same JSON envelope, and never retries them. Assert the expected status and result. For writes, read back the stored result. Never blindly retry a write whose outcome is unknown.\n\n| Code | Status | What to do |\n| --- | --- | --- |\n| `environment_not_running` | 409 | The environment is failed or deleted; `error.status` names it. Create another environment. |\n| `environment_starting` | 503 | For reads, wait `Retry-After` and retry. Check stored state before retrying an uncertain write. |\n| `environment_unavailable` | 503 | The app is restarting. For reads, wait `Retry-After`. Check stored state before retrying an uncertain write. |\n| `response_too_large` | 502 | The response is over the size limit. Ask for less, for example with a smaller page. |\n| `unsupported_protocol` | 501 | The app does not serve this API version or protocol. |\n\n## Quickstart\n\nUse the project's TypeScript runner and secret loader. For an empty Node.js project, save as `state-quickstart.mts` and run `npx tsx state-quickstart.mts` with the key available. The check prints the response status and deletes the environment. Only after it prints `200`, report verified setup and the rerun command. Continue only with an explicitly supplied task; otherwise ask what to run next and wait with no setup environment left running.\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATEMACHINES_API_KEY\nconst env = await sm.environments.create(\n { apps: ['salesforce'] },\n { wait: false },\n);\n\ntry {\n await sm.environments.wait(env.id, { status: 'running' });\n const salesforce = await env.connect('salesforce');\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.delete();\n}\n```\n\n## Docs\n\n- [Documentation](https://docs.usestatemachines.com): guides, concepts and reference ([llms.txt](https://docs.usestatemachines.com/llms.txt))\n- [npm package](https://www.npmjs.com/package/@usestatemachines/sdk): install, README and versions\n- [Agent skill](https://docs.usestatemachines.com/skill.md): workflow, rules and troubleshooting for coding agents\n- [OpenAPI document](https://cdn.jsdelivr.net/npm/@usestatemachines/sdk/openapi.json): the HTTP API the SDK calls\n- [State Machines](https://usestatemachines.com): website\n",
20
20
  skill:
21
- "---\nname: state-machines\ndescription: Use when an agent or test needs a disposable replica of a SaaS app such as Salesforce, to run integration tests or try API calls against a disposable copy instead of a real org. Covers the @usestatemachines/sdk TypeScript SDK - creating environments, credentials, choosing an actor, snapshots, pausing and resuming, cleanup, and errors such as environment_limit_reached and version_unavailable. Also use when the user mentions State Machines, STATEMACHINES_API_KEY or @usestatemachines/sdk.\n---\n\nRead `node_modules/@usestatemachines/sdk/README.md` before relying on memory; it matches the installed version. If the package is missing, run `npm install @usestatemachines/sdk`.\n\n## Workflow\n\n1. Check that `STATEMACHINES_API_KEY` is set without printing it (`test -n \"$STATEMACHINES_API_KEY\"`). If it isn't, ask the user to set it; the SDK doesn't read `.env` files.\n2. Create the environment with only the apps the task needs: `const env = await sm.environments.create({ apps: ['salesforce'] })`. Log `await sm.apps.list()` for each app's ID; don't guess IDs. `create` waits until the environment is `running`. If the start fails, the server deletes the environment and `create` throws `EnvironmentFailedError` with `err.environment.failure`.\n3. Open a `try` right after `create` returns, and call `await env.delete()` in its `finally`. It deletes the environment and its data.\n4. Get credentials for one app: `const crm = await env.connect('salesforce')`. This acts as the app's default actor, the Salesforce administrator. To act with another user's permissions, pass an `actorId` from the `actors` of the environment's version: the entry of `(await sm.apps.versions('salesforce')).data` whose `id` is `env.apps[0].versionId`. Salesforce actor IDs are its user IDs, such as `005…`, never a role name.\n5. Call the app with `crm.fetch(path)`. A leading `/` means the root of the app's `url`.\n\nThe whole flow, as the package ships it. It is ESM with top-level `await`: save it as `state-quickstart.mts` and run `npx tsx state-quickstart.mts`.\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATEMACHINES_API_KEY\nconst env = await sm.environments.create({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce');\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.delete();\n}\n```\n\n## Rules\n\n- Always delete in `finally`.\n- `env.pause()` releases the compute and keeps the data; it is not cleanup. `env.resume()` waits until `running` again. A paused or failed environment is deleted after one day, and still needs `env.delete()` once you are done.\n- To create without waiting, pass `{ wait: false }`, then `sm.environments.wait(env.id, { status: 'running' })`.\n- The time limit counts `starting` and `running`. `(await sm.me()).limits.timeLimitMinutes` gives its `default` and `maximum`. Set `timeLimitMinutes` in `create` for longer runs.\n- Running environments are capped per organization: `limits.concurrentEnvironments` from `sm.me()`. Delete the ones you no longer need. A paused or failed environment doesn't count until it resumes.\n- Never print, log or commit `STATEMACHINES_API_KEY`, `credentials.token` or `credentials.headers`.\n- Salesforce's SOAP API takes the actor token in the envelope, not a header: put `crm.token` in `SessionHeader/sessionId`. `crm.fetch` then sends only the environment token header.\n- `crm.fetch` never sends your API key, never follows redirects and only reaches URLs under the app's `url`.\n- Runtimes: Node.js, at the versions in the package's `engines` field, and Cloudflare Workers. ESM only. TypeScript needs `lib: [\"DOM\"]` or `@types/node`. Never ship an API key to a browser.\n- In Cloudflare Workers, pass the key explicitly: `new StateMachines({ apiKey: env.STATEMACHINES_API_KEY })`. A start takes longer than one request may last, so create with `{ wait: false }` and read `sm.environments.get(id)` in later requests until it is `running`, or pass a `signal` no shorter than the expected start.\n- Every `list` returns one page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`.\n- To reuse seeded data, `await env.snapshots.create({ name })` once, then `sm.environments.create({ snapshotId })` in place of `apps`.\n- Check errors with `instanceof` (`StateMachinesError`, `EnvironmentFailedError`, `SnapshotFailedError`, `UnexpectedStatusError`, `WaitTimeoutError`), not by `name`.\n\n## Recovering a failed create\n\nThe SDK already retries `502`, `503`, `504` and connection errors with one idempotency key. If you need to replay a create whose outcome is unknown, pass your own key: `sm.environments.create(input, { idempotencyKey })`, and call it again with the same input and key. It returns the environment the first call created, or creates it if that call never arrived. A different input fails with `idempotency_key_reused`.\n\n## Troubleshooting\n\n| Symptom | Cause | Fix |\n| --- | --- | --- |\n| `version_unavailable`, 422 | The app has no default version. | Check the app ID against `sm.apps.list()`. |\n| `environment_limit_reached`, 429 | The organization is at its cap of running environments. | List them with `sm.environments.list({ status: ['starting', 'running'] })`, delete or pause one, then create again. |\n| `invalid_status`, 409 | The status does not allow the call, for example pausing an environment that is `starting`. | Read `sm.environments.get(id)` and act on its `status`. |\n| `invalid_request`, 400 | A wrong field. | Read `err.fields`, keyed by dotted path. |\n| `EnvironmentFailedError` | The environment failed to start or resume. | Read `err.environment.failure`: `{ app, code, message }`. `invalid_settings` means the app refused its settings. |\n| `UnexpectedStatusError` | The environment was paused or deleted while the SDK waited. | Read `err.status` and `err.resource`. |\n| `WaitTimeoutError` | The status was not reached within `timeoutMs`. | Wait again with `sm.environments.wait(id, { status })`, or delete the environment. |\n| `snapshot_in_progress`, 409 | The environment already has a snapshot in `saving`. | `const [running] = (await sm.snapshots.list({ environmentId: env.id, status: ['saving'] })).data`, then `await sm.snapshots.wait(running.id, { status: 'ready' })`, then create another. |\n| `SnapshotFailedError` | The snapshot failed. `interrupted` means the environment was paused or deleted first. | Create another snapshot while the environment is `running`. |\n| `environment_not_running`, 409, from `crm.fetch` | The environment is paused, failed or deleted. `crm.fetch` returns it as a response and never retries. | Read `(await res.json()).error.status`; `env.resume()` a paused or failed environment. |\n| `environment_starting` or `environment_unavailable`, 503, from `crm.fetch` | The app is starting or restarting. | Wait the `Retry-After` seconds and call again. |\n| A vendor client such as jsforce is refused by the gateway | The gateway needs `StateMachines-Environment-Token` on every request. | Prefer `crm.fetch`. If you pass `crm.headers` to another client, send them only to `crm.url` and disable redirects. |\n| `TypeError: Pass apiKey or set STATEMACHINES_API_KEY.` | No key in the environment. | Ask the user to set `STATEMACHINES_API_KEY`; in Workers, pass `apiKey`. |\n\nMore: https://docs.usestatemachines.com, {{origin}}/llms.txt and https://www.npmjs.com/package/@usestatemachines/sdk\n",
21
+ "---\nname: state-machines\ndescription: Use when setting up State Machines or running code, integration tests, or API experiments against disposable SaaS replicas such as Salesforce. Applies to @usestatemachines/sdk and STATEMACHINES_API_KEY, including environment creation, actors, snapshots, cleanup and recovery.\n---\n\n## Start with the user's task\n\nDuring skill installation, read the repository instructions and preserve its existing structure. Do not create a new project, install dependencies, write examples or create remote resources. Report the installed skill location and any reload requirement, then ask \"What would you like to run or test?\" and wait. Skill installation does not verify a connection.\n\nOnce the user chooses a task, inspect the project's package manager, runtime, runner and secret loader. Install or reuse a compatible `@usestatemachines/sdk` only when the task needs it. Read the installed README for API signatures and `package.json` for runtime requirements. The SDK is ESM and runs on Node.js and Cloudflare Workers, not in a browser. Follow the user's task and existing project conventions without requiring a separate onboarding example.\n\n## Check access in the right process\n\nThe SDK reads `STATEMACHINES_API_KEY` from the process environment. It does not load `.env` files. Use the project's existing test command or secret loader. An empty variable in your current shell does not prove that the test process lacks it. Verify presence without displaying the value. In Workers, pass the binding explicitly with `new StateMachines({ apiKey: env.STATEMACHINES_API_KEY })`.\n\nCall `sm.me()` to verify access and workspace scope before creating resources. `me.limits` can be null. If the key is missing, prepare the local test or example and tell the user the exact local file, binding, or launch command to configure and rerun, without asking for the key in chat. Do not call the API until access is available. If access is denied or the workspace differs from the task, report the blocker. Do not change membership or permissions.\n\nNever print, log or commit API keys, `credentials.token`, `credentials.headers`, environment dumps, or secret-file contents. Use synthetic test data.\n\n## Run the user’s chosen task\n\nApply this section after setup, once the user has chosen a task.\n\n- `sm.apps.list()` returns app IDs. Every list returns `{ data, nextCursor }`. Follow cursors until the required entry is found or `nextCursor` is null.\n- Create only the apps the task needs. A string such as `'salesforce'` is both its app ID and its name in the environment. For named apps, later `connect()` calls use the name.\n- `connect(appName)` uses the app's default actor, the Salesforce administrator. For another actor, find the matching app by name in `environment.apps`, read its `versionId`, then find that exact version through `sm.apps.versions(appId)`. Use an actor ID from its `actors`, never a role label, workspace member ID, or the first version in a list.\n- Check the declared APIs and [known differences](https://docs.usestatemachines.com/apps/known-differences.md) for the behavior under test. A replica does not import a customer's org or run arbitrary customer automation.\n- Configure the existing integration's test transport with the returned app URL and credentials. Verify its requests reach that URL. A successful standalone SDK request does not prove the integration uses the replica. If transport configuration needs a broader change, explain it before expanding the task.\n- Check the expected HTTP status and assert the result. For writes, read the stored data back and assert the relevant fields. For negative tests, assert the expected error. Do not weaken an assertion to make unsupported behavior pass.\n\n## Own the full lifecycle\n\nCreate with `{ wait: false }` to retain the accepted environment ID before waiting. Enter `try` immediately, then wait for `running`, connect and run the check inside it. Delete in `finally`, including after a startup failure, timeout or failed assertion. An aborted wait does not cancel server work. Cleanup must use a fresh, un-aborted signal. Preserve both verification and cleanup errors, and report the environment ID if deletion fails.\n\nDelete only environments created for this task. If the organization is at its limit, clean up a known leftover from this task or ask the user to free capacity. Do not delete unrelated environments or a user-supplied reusable snapshot.\n\nThe SDK retries management connection errors and `502`, `503`, `504` with one idempotency key per create call. To recover across separate attempts or a restart, retain the input and an explicit `idempotencyKey` before creation. Replay using the same input, key, workspace and caller. Do not create a replacement with a new key while the first outcome is unknown. See [operation recovery](https://docs.usestatemachines.com/sdk-api/recovery.md).\n\n## Optional connection check\n\nRun this only when the user asks to verify the connection. It proves the connection and cleanup, not an existing integration's behavior. Save it in the project's examples or test location and run it with the existing TypeScript runner and secret loader. In a new Node.js project, `state-quickstart.mts` and `npx tsx state-quickstart.mts` are a fallback. Load the key into that process first.\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATEMACHINES_API_KEY\nconst env = await sm.environments.create(\n { apps: ['salesforce'] },\n { wait: false },\n);\n\ntry {\n await sm.environments.wait(env.id, { status: 'running' });\n const salesforce = await env.connect('salesforce');\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.delete();\n}\n```\n\n## Load details when needed\n\n- **Repeated fixtures or parallel tests:** Read [isolated tests](https://docs.usestatemachines.com/sdk-api/testing.md). Save seeded data with `env.snapshots.create({ name })`, then create each test's environment with `{ snapshotId }`. A snapshot in `saving` cannot be deleted yet. Retain its ID and wait for `ready` or `failed` before cleanup. Delete a reusable snapshot only when the task calls for it.\n- **Another actor, vendor client or SOAP:** Read [credentials](https://docs.usestatemachines.com/environments/credentials.md). `credentials.fetch()` confines requests to the app URL, adds its authentication headers, and never follows redirects. External clients must send the environment headers only to that URL and disable redirects. For Salesforce SOAP, put the actor token in `SessionHeader/sessionId` in the envelope.\n- **Cloudflare Workers:** Pass the key explicitly. Use `{ wait: false }` and keep the environment ID across requests or in the job's state. A request ending does not cancel startup. Follow the worker's background-job lifecycle for polling and cleanup.\n- **Long runs:** Read `sm.me().limits` when non-null. The environment time limit covers starting and running. Use a supported `timeLimitMinutes` when the task needs it.\n\n## Recover without changing the test's meaning\n\nManagement failures throw SDK errors. App HTTP failures are returned as `Response` objects. Use `instanceof` with the SDK error classes. Native `credentials.fetch()` never retries. Read the gateway error code and distinguish it from an app's own error.\n\n| Failure | Next action |\n| --- | --- |\n| `invalid_request` | Read `err.fields` and the installed method signature. |\n| `version_unavailable` | Discover the app ID and available versions. Stop if the required version is unavailable. |\n| `environment_limit_reached` | Clean up only this task's known leftovers, or report the capacity blocker. |\n| `EnvironmentFailedError` | Read `err.environment.failure` and clean up the owned environment. |\n| `WaitTimeoutError` or abort | Use the retained environment ID. If older code waited inside `create`, a non-null `WaitTimeoutError.resource` carries its ID. If neither an ID nor a replay key is recoverable, report unresolved cleanup. Do not guess from a list. Wait again only if the task should continue. Otherwise delete the owned environment. |\n| Unknown create outcome | Replay the retained input and idempotency key. Do not start another operation with a new key. |\n| `invalid_status` | Read the current resource status before choosing the next call. |\n| `snapshot_in_progress` | Find the saving snapshot for this environment and wait for it before creating another. |\n| `SnapshotFailedError` | Report the failure. Retry only if the task still needs a snapshot and the source environment is running. |\n| App `environment_not_running` | Inspect the environment. Do not silently switch the integration to another target. |\n| App or gateway `503` | For a retryable read, honor `Retry-After`. For a write with an uncertain outcome, inspect stored state or report uncertainty before retrying. |\n| Cleanup failure | Preserve the task result, report the owned resource ID and retry deletion with valid access and a fresh signal. Do not report cleanup as successful. |\n\n## Finish with evidence\n\nLeave the smallest rerunnable test or example. Report the behavior checked, command, assertion result, SDK and app versions, selected actor, resource IDs without secrets, and cleanup outcome. Say when execution was blocked or only typechecking ran. A passing replica check establishes the asserted behavior for that version and actor, not full production compatibility.\n\nFind other guides through https://docs.usestatemachines.com/llms.txt. Read focused pages before the full documentation bundle. This dashboard also serves an agent index at {{origin}}/llms.txt.\n",
22
22
  });
package/agents/llms.txt CHANGED
@@ -1,17 +1,16 @@
1
1
  # State Machines
2
2
 
3
- > State Machines runs disposable environments of replica SaaS apps, such as Salesforce, for tests and coding agents. Create, connect to, pause, resume and delete them with the TypeScript SDK `@usestatemachines/sdk`.
3
+ > State Machines runs disposable environments of replica SaaS apps, such as Salesforce, for tests and coding agents. Create, connect to, and delete them with the TypeScript SDK `@usestatemachines/sdk`.
4
4
 
5
- Install with `npm install @usestatemachines/sdk`. It is ESM only and runs on Node.js and Cloudflare Workers, not browsers. `engines` in its `package.json` gives the Node.js versions. TypeScript needs `lib: ["DOM"]` or `@types/node`. `new StateMachines()` reads the API key from `STATEMACHINES_API_KEY` and doesn't read `.env` files; in Cloudflare Workers, pass `apiKey` explicitly. Never print the key. The installed `node_modules/@usestatemachines/sdk/README.md` matches your version: read it before relying on memory.
5
+ Use the project's package manager to install or reuse `@usestatemachines/sdk`. It is ESM only and runs on Node.js and Cloudflare Workers, not browsers. `engines` in its `package.json` gives the Node.js versions. TypeScript needs `lib: ["DOM"]` or `@types/node`. `new StateMachines()` reads the API key from `STATEMACHINES_API_KEY` and doesn't read `.env` files; in Cloudflare Workers, pass `apiKey` explicitly. Never print the key. The installed `node_modules/@usestatemachines/sdk/README.md` matches your version: read it before relying on memory. Read the bundled `agents/SKILL.md` for the task workflow. Reuse the project's secret loader and check the key in the process that runs the test, not just the current shell. Installing the skill does not require SDK installation, example files or remote resources. Ask what the user wants to run and wait. Install the SDK and verify access when their chosen task needs it. Run the connection check below only if the user asks to verify the connection. Do not choose a business workflow from the repository.
6
6
 
7
7
  ## Concepts
8
8
 
9
- - Environment: one disposable run of one or more apps. `const env = await sm.environments.create({ apps: ['salesforce'] })` creates it and waits until it is `running`. Pass `{ wait: false }` to get it back at once in `starting`. Always call `env.delete()` in a `finally`.
9
+ - Environment: one disposable run of one or more apps. `const env = await sm.environments.create({ apps: ['salesforce'] })` creates it and waits until it is `running`. Use `{ wait: false }` to retain the ID before startup. Wait for `running` inside `try`, then delete the owned environment in `finally`, including after a wait failure.
10
10
  - Apps: a string names the app, and the app's name in the environment is the same string. Use `{ app: 'salesforce', name: 'crm', settings }` for your own name, for two copies of one app, or for settings. Later calls take the name.
11
- - Pause and resume: `env.pause()` releases the compute and keeps the data; the time limit stops counting. `env.resume()` boots the apps again on the same data and waits until `running`. A paused environment is deleted after one day.
12
11
  - Credentials: `const crm = await env.connect('salesforce')` returns `url`, `headers`, `token` and `fetch`. `crm.fetch(path)` joins `path` to `url`, adds `headers`, and puts `token` in the header of the API whose path matches: `Authorization: Bearer` for Salesforce REST and Bulk 2.0. For Salesforce SOAP, put `crm.token` in the envelope's `SessionHeader/sessionId` yourself. `fetch` never sends your API key, never follows redirects and never retries. `token` and `headers` are secrets.
13
- - Actor: one of the app's users; requests run with that user's permissions. `connect()` acts as the app's default actor (for Salesforce, the administrator) unless you pass `actorId`. Salesforce actor IDs are its user IDs, such as `005…`. Read them from the `actors` of the environment's version: the entry of `(await sm.apps.versions('salesforce')).data` whose `id` is `env.apps[0].versionId`.
14
- - Snapshot: the saved data of every app of a running environment, taken at one moment. `await env.snapshots.create({ name: 'seeded' })` waits until it is `ready`. `sm.environments.create({ snapshotId })` creates a new environment from it, in place of `apps`; each app keeps its name and version. A pause or delete during the snapshot fails it with `interrupted`.
12
+ - Actor: one of the app's users; requests run with that user's permissions. `connect()` acts as the app's default actor (for Salesforce, the administrator) unless you pass `actorId`. Salesforce actor IDs are its user IDs, such as `005…`. Find the app by name in `env.apps`, then page through `sm.apps.versions(appId)` to find its exact `versionId`. Read actor IDs from that version's `actors`.
13
+ - Snapshot: the saved data of every app of a running environment, taken at one moment. `await env.snapshots.create({ name: 'seeded' })` waits until it is `ready`. `sm.environments.create({ snapshotId })` creates a new environment from it, in place of `apps`; each app keeps its name and version. A delete during the snapshot fails it with `interrupted`.
15
14
  - Recorded requests: `sm.requests.list({ environmentId })` pages the calls made to the environment's apps. `sm.requests.get(id)` adds headers and a preview of each body. `sm.requests.body(id, 'request')` or `'response'` returns the full body as bytes. Recording is best effort.
16
15
  - YAML: `parseEnvironmentYaml(text)` turns a YAML document with the fields of `create` into its input.
17
16
  - Pagination: every `list` returns one page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`.
@@ -20,14 +19,16 @@ Install with `npm install @usestatemachines/sdk`. It is ESM only and runs on Nod
20
19
 
21
20
  Enums are open: handle values you don't recognise.
22
21
 
23
- - Environment `status`: `starting`, `running`, `paused`, `failed`, `deleted`. A `deleted` environment has a `reason` when State Machines deleted it: `expired`, `failed` (it never reached `running`) or `cancelled` (staff ended it). The `reason` is `null` when a caller deleted it. A `failed` environment keeps its data for `resume()` and has `failure`: `{ app, code, message }` with `start_failed`, `resume_failed`, `restart_failed`, `version_unavailable` or `invalid_settings`.
22
+ - Environment `status`: `starting`, `running`, `failed`, `deleted`. A `deleted` environment has a `reason` when State Machines deleted it: `expired`, `failed` (it never reached `running`) or `cancelled` (staff ended it). The `reason` is `null` when a caller deleted it. A `failed` environment has `failure`: `{ app, code, message }` with `start_failed`, `restart_failed`, `version_unavailable` or `invalid_settings`.
24
23
  - Snapshot `status`: `saving`, `ready`, `failed`, `deleted`. A failed snapshot has `failure.code` `interrupted` or `internal_error`.
25
- - `create` and `resume` throw `EnvironmentFailedError` when the environment fails, `UnexpectedStatusError` when it becomes `paused` or `deleted` for another reason, and `WaitTimeoutError` when `timeoutMs` passes.
24
+ - `create` throws `EnvironmentFailedError` when the environment fails, `UnexpectedStatusError` when it becomes `deleted` for another reason, and `WaitTimeoutError` when `timeoutMs` passes.
26
25
  - `snapshots.create` throws `SnapshotFailedError` when the snapshot fails and `UnexpectedStatusError` when it was deleted.
27
26
  - `sm.environments.wait(id, { status })` and `sm.snapshots.wait(id, { status })` wait for any status, and throw once it cannot be reached without another call.
28
27
 
29
28
  ## Limits
30
29
 
30
+ When `sm.me().limits` is non-null:
31
+
31
32
  - Running environments are capped per organization: `(await sm.me()).limits.concurrentEnvironments` counts `starting` and `running`.
32
33
  - `timeLimitMinutes` has a default and a maximum: `(await sm.me()).limits.timeLimitMinutes`. It counts `starting` and `running` only; at the end the environment is deleted with `reason: 'expired'`.
33
34
 
@@ -40,39 +41,42 @@ A failed API call throws `StateMachinesError` with `status`, `code`, `message`,
40
41
  | `invalid_request` | 400 | Read `fields`, keyed by the dotted path of each wrong field. |
41
42
  | `forbidden` | 403 | The key lacks the permission. |
42
43
  | `not_found` | 404 | Check the ID and the workspace. |
43
- | `invalid_status` | 409 | The status does not allow the call: pause needs `running`; resume needs `paused` or `failed`. |
44
+ | `invalid_status` | 409 | The status does not allow the call. Snapshots need a running environment. |
44
45
  | `snapshot_in_progress` | 409 | Wait for the running snapshot, then create another. |
45
46
  | `idempotency_key_reused` | 409 | The key was used with a different input. Replay with the exact same input. |
46
47
  | `workspace_archived` | 409 | The workspace is archived. |
47
48
  | `body_too_large` | 413 | The request body is over the API's size limit. Send less. |
48
49
  | `version_unavailable` | 422 | The app has no default version. |
49
- | `environment_limit_reached` | 429 | Pause or delete an environment, then create or resume again. |
50
- | `unavailable` | 503 | Retried automatically. If `create` still fails, call it again with the same input and your own `idempotencyKey`. |
50
+ | `environment_limit_reached` | 429 | Delete only this task’s known leftovers, or ask the user to free capacity. |
51
+ | `unavailable` | 503 | Retried automatically. If `create` still has an unknown outcome, replay the same input with the key retained before the original call. Do not generate a new key. |
51
52
 
52
53
  ## Errors from the app URL
53
54
 
54
- `crm.fetch` returns these as responses with the same JSON envelope, and never retries them.
55
+ `crm.fetch` returns these as responses with the same JSON envelope, and never retries them. Assert the expected status and result. For writes, read back the stored result. Never blindly retry a write whose outcome is unknown.
55
56
 
56
57
  | Code | Status | What to do |
57
58
  | --- | --- | --- |
58
- | `environment_not_running` | 409 | The environment is paused, failed or deleted; `error.status` names it. Resume a paused or failed environment. |
59
- | `environment_starting` | 503 | Wait `Retry-After` and call again. |
60
- | `environment_unavailable` | 503 | The app is restarting. Wait `Retry-After` and call again. |
59
+ | `environment_not_running` | 409 | The environment is failed or deleted; `error.status` names it. Create another environment. |
60
+ | `environment_starting` | 503 | For reads, wait `Retry-After` and retry. Check stored state before retrying an uncertain write. |
61
+ | `environment_unavailable` | 503 | The app is restarting. For reads, wait `Retry-After`. Check stored state before retrying an uncertain write. |
61
62
  | `response_too_large` | 502 | The response is over the size limit. Ask for less, for example with a smaller page. |
62
- | `capacity_exceeded` | 429 | Too many requests in flight to the app. Send fewer at once. |
63
63
  | `unsupported_protocol` | 501 | The app does not serve this API version or protocol. |
64
64
 
65
65
  ## Quickstart
66
66
 
67
- Save it unchanged as `state-quickstart.mts` and run `npx tsx state-quickstart.mts`.
67
+ Use the project's TypeScript runner and secret loader. For an empty Node.js project, save as `state-quickstart.mts` and run `npx tsx state-quickstart.mts` with the key available. The check prints the response status and deletes the environment. Only after it prints `200`, report verified setup and the rerun command. Continue only with an explicitly supplied task; otherwise ask what to run next and wait with no setup environment left running.
68
68
 
69
69
  ```ts
70
70
  import { StateMachines } from '@usestatemachines/sdk';
71
71
 
72
72
  const sm = new StateMachines(); // reads STATEMACHINES_API_KEY
73
- const env = await sm.environments.create({ apps: ['salesforce'] });
73
+ const env = await sm.environments.create(
74
+ { apps: ['salesforce'] },
75
+ { wait: false },
76
+ );
74
77
 
75
78
  try {
79
+ await sm.environments.wait(env.id, { status: 'running' });
76
80
  const salesforce = await env.connect('salesforce');
77
81
  const res = await salesforce.fetch('/services/data/v67.0/limits');
78
82
  console.log(res.status, await res.json());
@@ -85,6 +89,6 @@ try {
85
89
 
86
90
  - [Documentation](https://docs.usestatemachines.com): guides, concepts and reference ([llms.txt](https://docs.usestatemachines.com/llms.txt))
87
91
  - [npm package](https://www.npmjs.com/package/@usestatemachines/sdk): install, README and versions
88
- - [Agent skill](https://app.usestatemachines.com/skill.md): workflow, rules and troubleshooting for coding agents
92
+ - [Agent skill](https://docs.usestatemachines.com/skill.md): workflow, rules and troubleshooting for coding agents
89
93
  - [OpenAPI document](https://cdn.jsdelivr.net/npm/@usestatemachines/sdk/openapi.json): the HTTP API the SDK calls
90
94
  - [State Machines](https://usestatemachines.com): website
package/agents/prompt.md CHANGED
@@ -1,26 +1,6 @@
1
- Set up State Machines so this project can run code against a disposable replica of a SaaS app, such as Salesforce. Follow these steps in order.
1
+ Install the State Machines skill in this existing repository so I can use disposable app environments with my coding agent.
2
2
 
3
- 1. Install the SDK: `npm install @usestatemachines/sdk`. It is ESM only and runs on Node.js, at the versions in its `engines` field, and on Cloudflare Workers, not in browsers.
4
- 2. Check that `STATEMACHINES_API_KEY` is set without printing it: `test -n "$STATEMACHINES_API_KEY"`. If it is missing, stop and ask me to set it; the SDK doesn't read `.env` files. In Cloudflare Workers, pass the key as `new StateMachines({ apiKey })`.
5
- 3. Save this quickstart unchanged as `state-quickstart.mts` and run `npx tsx state-quickstart.mts` to confirm the setup. It prints `200` and the Salesforce org's limits:
6
-
7
- ```ts
8
- import { StateMachines } from '@usestatemachines/sdk';
9
-
10
- const sm = new StateMachines(); // reads STATEMACHINES_API_KEY
11
- const env = await sm.environments.create({ apps: ['salesforce'] });
12
-
13
- try {
14
- const salesforce = await env.connect('salesforce');
15
- const res = await salesforce.fetch('/services/data/v67.0/limits');
16
- console.log(res.status, await res.json());
17
- } finally {
18
- await env.delete();
19
- }
20
- ```
21
-
22
- 4. Adapt it to my task. Log `await sm.apps.list()` to find each app's ID, and `await sm.apps.versions(appId)` for the actors and APIs of its versions; don't guess IDs. Create only the apps the task needs; a string such as `'salesforce'` names the app and also becomes its name in the environment. `connect()` acts as the app's default actor, the Salesforce administrator; to act as another user, pass an `actorId` from the version's `actors`. To reuse seeded data, create a snapshot once with `await env.snapshots.create()`, then `sm.environments.create({ snapshotId })`.
23
- 5. Always call `env.delete()` in a `finally`; it deletes the environment and its data. If the start fails, the server deletes the environment itself and `create` throws `EnvironmentFailedError`. Pass your own `idempotencyKey` to `create`; if it throws with no response, call it again with the same input and key.
24
- 6. Never print, log or commit keys or tokens, including `credentials.token` and `credentials.headers`.
25
-
26
- Read more: https://docs.usestatemachines.com, `node_modules/@usestatemachines/sdk/README.md`, https://app.usestatemachines.com/llms.txt and https://www.npmjs.com/package/@usestatemachines/sdk
3
+ 1. Read the repository instructions. Download the skill from https://docs.usestatemachines.com/skill.md and install it as `state-machines/SKILL.md` in this agent's supported project-local skills directory. Reuse an identical installed copy. Preserve customizations and ask before replacing a different copy. If persistent skills are unsupported, read it for this session and explain the limitation. If the download fails, report the blocker.
4
+ 2. Read the skill. Do not create a new project, install the SDK, write example code, change an integration or create remote resources during skill installation. Keep the repository's existing conventions.
5
+ 3. The API key belongs in the project's existing secret configuration as `STATEMACHINES_API_KEY`. Never ask me to paste it into chat, print it or commit it. If I need help configuring it, identify the existing secret loader and explain where to save it without reading or displaying secret values.
6
+ 4. Report where the skill was installed and whether the agent needs to reload it. Skill installation alone does not verify an API connection. Ask: "What would you like to run or test?" Wait for my answer before making further changes.
@@ -40,12 +40,6 @@ export interface CreateEnvironmentOptions extends SignalOptions {
40
40
  /** A replay with the same key and input returns the same environment. Defaults to a new UUID. */
41
41
  idempotencyKey?: string;
42
42
  }
43
- export interface ResumeOptions extends SignalOptions {
44
- /** Wait until the environment is `running`. Defaults to true. */
45
- wait?: boolean;
46
- /** Deadline for the wait. Defaults to 10 minutes. */
47
- timeoutMs?: number;
48
- }
49
43
  export type CreateSnapshotOptions = CreateEnvironmentOptions;
50
44
  export interface ConnectOptions {
51
45
  /** Defaults to the app's default actor (for Salesforce, the administrator). */
@@ -87,10 +81,6 @@ export declare class StateMachines {
87
81
  get: (environmentId: string) => Promise<Environment>;
88
82
  /** `labels` replaces the whole map. */
89
83
  update: (environmentId: string, body: UpdateEnvironmentRequestBody) => Promise<Environment>;
90
- /** Takes effect when the API answers: the gateway refuses calls from then on. */
91
- pause: (environmentId: string, options?: SignalOptions) => Promise<Environment>;
92
- /** Resumes a paused or failed environment and, unless `wait` is false, waits until it is `running`. */
93
- resume: (environmentId: string, options?: ResumeOptions) => Promise<Environment>;
94
84
  /** Deletes the environment and its data. Safe to repeat. */
95
85
  delete: (environmentId: string, options?: SignalOptions) => Promise<Environment>;
96
86
  /**
@@ -72,18 +72,6 @@ export class StateMachines {
72
72
  get: async (environmentId) => this.#bind(await this.#call(undefined, (call) => this.#sdk.environments.get({ environmentId }, call))),
73
73
  /** `labels` replaces the whole map. */
74
74
  update: async (environmentId, body) => this.#bind(await this.#call(undefined, (call) => this.#sdk.environments.update({ environmentId, body }, call))),
75
- /** Takes effect when the API answers: the gateway refuses calls from then on. */
76
- pause: async (environmentId, options = {}) => this.#bind(await this.#call(options.signal, (call) => this.#sdk.environments.pause({ environmentId }, call))),
77
- /** Resumes a paused or failed environment and, unless `wait` is false, waits until it is `running`. */
78
- resume: async (environmentId, options = {}) => {
79
- const resumed = await this.#call(options.signal, (call) => this.#sdk.environments.resume({ environmentId }, call));
80
- return options.wait === false
81
- ? this.#bind(resumed)
82
- : this.environments.wait(environmentId, {
83
- status: 'running',
84
- ...waitOptions(options),
85
- });
86
- },
87
75
  /** Deletes the environment and its data. Safe to repeat. */
88
76
  delete: async (environmentId, options = {}) => this.#bind(await this.#call(options.signal, (call) => this.#sdk.environments.delete({ environmentId }, call))),
89
77
  /**
@@ -186,8 +174,6 @@ export class StateMachines {
186
174
  const { id } = data;
187
175
  const helpers = {
188
176
  connect: (app, options = {}) => this.#connect(id, data, app, options),
189
- pause: (options) => this.environments.pause(id, options),
190
- resume: (options) => this.environments.resume(id, options),
191
177
  delete: (options) => this.environments.delete(id, options),
192
178
  refresh: () => this.environments.get(id),
193
179
  snapshots: {
@@ -1,6 +1,6 @@
1
1
  import type { Environment as EnvironmentData } from '../models/environment.js';
2
2
  import type { Snapshot } from '../models/snapshot.js';
3
- import type { ConnectOptions, CreateSnapshotOptions, ResumeOptions, SignalOptions } from './client.js';
3
+ import type { ConnectOptions, CreateSnapshotOptions, SignalOptions } from './client.js';
4
4
  import type { Credentials } from './credentials.js';
5
5
  /**
6
6
  * An environment as the API returned it, with helpers bound to it. The helpers are not
@@ -10,9 +10,6 @@ export type Environment = EnvironmentData & EnvironmentHelpers;
10
10
  export interface EnvironmentHelpers {
11
11
  /** Credentials for one app; `actorId` defaults to the app's default actor. */
12
12
  connect(app: string, options?: ConnectOptions): Promise<Credentials>;
13
- pause(options?: SignalOptions): Promise<Environment>;
14
- /** Waits until `running` unless `wait` is false. */
15
- resume(options?: ResumeOptions): Promise<Environment>;
16
13
  delete(options?: SignalOptions): Promise<Environment>;
17
14
  /** Reads the environment again. */
18
15
  refresh(): Promise<Environment>;
@@ -1,5 +1,5 @@
1
1
  export type { Actor, Api, App, AuditEvent, BodyPreview, CreateEnvironment, CreateEnvironmentApp, EnvironmentApp, EnvironmentFailure, EnvironmentFailureCode, EnvironmentStatus, ErrorCode, Me, Permission, Principal, RequestDetail, RequestStats, RequestT as Request, Snapshot, SnapshotStatus, Version, Workspace, } from '../models/index.js';
2
- export type { ConnectOptions, CreateEnvironmentOptions, CreateSnapshotOptions, ListAuditEventsQuery, ListEnvironmentsQuery, ListRequestsQuery, ListSnapshotsQuery, ListWorkspacesQuery, Page, PageQuery, RequestStatsQuery, ResumeOptions, SignalOptions, StateMachinesOptions, } from './client.js';
2
+ export type { ConnectOptions, CreateEnvironmentOptions, CreateSnapshotOptions, ListAuditEventsQuery, ListEnvironmentsQuery, ListRequestsQuery, ListSnapshotsQuery, ListWorkspacesQuery, Page, PageQuery, RequestStatsQuery, SignalOptions, StateMachinesOptions, } from './client.js';
3
3
  export { StateMachines } from './client.js';
4
4
  export type { Credentials } from './credentials.js';
5
5
  export type { Environment, EnvironmentHelpers } from './environment.js';
@@ -2,14 +2,10 @@ import { EnvironmentFailedError, isTransient, SnapshotFailedError, toStateMachin
2
2
  import { linkSignal } from './signal.js';
3
3
  export const defaultTimeoutMs = 600000;
4
4
  const readTimeoutMs = 30000;
5
- /**
6
- * The statuses each status reaches with no call from the customer: a boot ends, a restart
7
- * fails, the time limit passes, a paused or failed environment expires after a day.
8
- */
5
+ /** Statuses reachable without another customer request. */
9
6
  const environmentReaches = {
10
7
  starting: ['running', 'failed', 'deleted'],
11
8
  running: ['failed', 'deleted'],
12
- paused: ['deleted'],
13
9
  failed: ['deleted'],
14
10
  deleted: [],
15
11
  };
@@ -31,7 +31,7 @@ export declare function serverURLFromOptions(options: SDKOptions): URL | null;
31
31
  export declare const SDK_METADATA: {
32
32
  readonly language: "typescript";
33
33
  readonly openapiDocVersion: "1.0.0";
34
- readonly sdkVersion: "0.6.1";
34
+ readonly sdkVersion: "0.7.1";
35
35
  readonly genVersion: "2.941.0";
36
- readonly userAgent: "speakeasy-sdk/typescript 0.6.1 2.941.0 1.0.0 @usestatemachines/sdk";
36
+ readonly userAgent: "speakeasy-sdk/typescript 0.7.1 2.941.0 1.0.0 @usestatemachines/sdk";
37
37
  };
package/esm/lib/config.js CHANGED
@@ -24,7 +24,7 @@ export function serverURLFromOptions(options) {
24
24
  export const SDK_METADATA = {
25
25
  language: "typescript",
26
26
  openapiDocVersion: "1.0.0",
27
- sdkVersion: "0.6.1",
27
+ sdkVersion: "0.7.1",
28
28
  genVersion: "2.941.0",
29
- userAgent: "speakeasy-sdk/typescript 0.6.1 2.941.0 1.0.0 @usestatemachines/sdk",
29
+ userAgent: "speakeasy-sdk/typescript 0.7.1 2.941.0 1.0.0 @usestatemachines/sdk",
30
30
  };
@@ -4,8 +4,7 @@ import { Result as SafeParseResult } from "../types/fp.js";
4
4
  import { SDKValidationError } from "./errors/sdk-validation-error.js";
5
5
  export declare const EnvironmentFailureCode: {
6
6
  readonly StartFailed: "start_failed";
7
- readonly RestartFailed: "restart_failed";
8
- readonly ResumeFailed: "resume_failed";
7
+ readonly PauseRemoved: "pause_removed";
9
8
  readonly VersionUnavailable: "version_unavailable";
10
9
  readonly InvalidSettings: "invalid_settings";
11
10
  };