---
name: state-machines
description: 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.
---

Read `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`.

## Workflow

1. 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.
2. 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`.
3. Open a `try` right after `create` returns, and call `await env.delete()` in its `finally`. It deletes the environment and its data.
4. 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.
5. Call the app with `crm.fetch(path)`. A leading `/` means the root of the app's `url`.

The 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`.

```ts
import { StateMachines } from '@usestatemachines/sdk';

const sm = new StateMachines(); // reads STATEMACHINES_API_KEY
const env = await sm.environments.create({ apps: ['salesforce'] });

try {
  const salesforce = await env.connect('salesforce');
  const res = await salesforce.fetch('/services/data/v67.0/limits');
  console.log(res.status, await res.json());
} finally {
  await env.delete();
}
```

## Rules

- Always delete in `finally`.
- `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.
- To create without waiting, pass `{ wait: false }`, then `sm.environments.wait(env.id, { status: 'running' })`.
- The time limit counts `starting` and `running`. `(await sm.me()).limits.timeLimitMinutes` gives its `default` and `maximum`. Set `timeLimitMinutes` in `create` for longer runs.
- 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.
- Never print, log or commit `STATEMACHINES_API_KEY`, `credentials.token` or `credentials.headers`.
- 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.
- `crm.fetch` never sends your API key, never follows redirects and only reaches URLs under the app's `url`.
- 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.
- 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.
- Every `list` returns one page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`.
- To reuse seeded data, `await env.snapshots.create({ name })` once, then `sm.environments.create({ snapshotId })` in place of `apps`.
- Check errors with `instanceof` (`StateMachinesError`, `EnvironmentFailedError`, `SnapshotFailedError`, `UnexpectedStatusError`, `WaitTimeoutError`), not by `name`.

## Recovering a failed create

The 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`.

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| `version_unavailable`, 422 | The app has no default version. | Check the app ID against `sm.apps.list()`. |
| `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. |
| `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`. |
| `invalid_request`, 400 | A wrong field. | Read `err.fields`, keyed by dotted path. |
| `EnvironmentFailedError` | The environment failed to start or resume. | Read `err.environment.failure`: `{ app, code, message }`. `invalid_settings` means the app refused its settings. |
| `UnexpectedStatusError` | The environment was paused or deleted while the SDK waited. | Read `err.status` and `err.resource`. |
| `WaitTimeoutError` | The status was not reached within `timeoutMs`. | Wait again with `sm.environments.wait(id, { status })`, or delete the environment. |
| `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. |
| `SnapshotFailedError` | The snapshot failed. `interrupted` means the environment was paused or deleted first. | Create another snapshot while the environment is `running`. |
| `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. |
| `environment_starting` or `environment_unavailable`, 503, from `crm.fetch` | The app is starting or restarting. | Wait the `Retry-After` seconds and call again. |
| 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. |
| `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`. |

More: https://app.usestatemachines.com/llms.txt and https://www.npmjs.com/package/@usestatemachines/sdk
