# State Machines documentation # What is State Machines? Disposable environments of replica apps for integration tests and agents. State Machines gives your code an isolated environment of replica apps. A replica implements a vendor's APIs, such as Salesforce. Your integration creates records, reads them, and tests failures against disposable data. You create an environment, connect to an app, make native API calls, and delete the environment when the task ends. To repeat the task, create a fresh environment or start one from a snapshot of prepared data. A replica never calls the vendor. Creating a Salesforce app does not connect to your company's Salesforce organization or import its records. [Resources and how they relate](/get-started/concepts/) explains environments, apps, actors, and snapshots. ## Two APIs have different jobs The State Machines management API creates environments, returns credentials, and manages snapshots. Your State Machines API key authenticates those calls within a workspace. The app's native API handles your integration's work. For Salesforce, that includes record operations, SOQL queries, and supported Bulk and SOAP operations. Credentials identify both the environment and an actor inside the app. Your State Machines API key is never an app credential. ## Match the starting point to the task | Your task | Start with | Verify | | --- | --- | --- | | Make a first native app request | [The SDK quickstart](/get-started/quickstart/) | The HTTP response and environment cleanup | | Run an existing integration test | [An isolated test helper](/sdk-api/testing/) | Your integration's assertions, with its base URL and credentials set to the replica | | Repeat a scenario with prepared records | [A ready snapshot](/snapshots/reuse/) | The restored records and independence between test environments | | Let an agent exercise the integration | [Agent instructions](/get-started/agents/) | The executed command, observed result, and cleanup outcome | | Diagnose a failing run | [Requests and audit events](/environments/observability/) | The failing app call separately from the management operation | To use the same resources through the UI, follow [Run an environment in the dashboard](/get-started/dashboard/). ## Know what a passing test proves A passing test proves the assertions it ran against the selected replica version, settings, actor, and data. It does not prove that every feature of the vendor product behaves the same, or that the replica reproduces a customer's configuration. Record the replica version, actor, and data with each result. Before testing customer-specific behavior, check [Salesforce known differences](/apps/known-differences/) for the features it needs. --- # Make your first app request Create a Salesforce environment, read its limits, and delete the environment with the TypeScript SDK. We create a Salesforce environment, read its REST limits, and delete it. The `finally` block deletes the environment even if startup or the request fails. ## Install the SDK Install `@usestatemachines/sdk` 0.6.0 or later. The SDK runs on Node.js 20.20 or later and is ESM only. ```sh npm install @usestatemachines/sdk ``` If your project uses version 0.5 or earlier, follow [Upgrade from SDK 0.5 to 0.6](/sdk-api/upgrading/) first. Those versions call routes that the API no longer serves. ## Set your API key In the [dashboard](https://app.usestatemachines.com), select the workspace for the environment. Open **API keys**, create a key, and save its value when it appears. If you cannot create a key, see [workspace access](/environments/access/). Set `STATEMACHINES_API_KEY` in the process that runs your script. Use your terminal's secret handling or your CI secret store. The SDK reads the process environment. It does not load `.env` files. Check that the variable exists without printing it: ```sh test -n "$STATEMACHINES_API_KEY" ``` ## Run the request The example creates the environment with `wait: false`, so it gets the environment ID before startup finishes. It then waits for `running` inside `try`. `connect('salesforce')` returns credentials for the default actor, the Salesforce administrator. ```ts import { StateMachines } from '@usestatemachines/sdk'; const sm = new StateMachines(); const env = await sm.environments.create( { apps: ['salesforce'] }, { wait: false }, ); try { const running = await sm.environments.wait(env.id, { status: 'running' }); const salesforce = await running.connect('salesforce'); const response = await salesforce.fetch('/services/data/v67.0/limits'); if (!response.ok) { throw new Error(`Salesforce returned ${response.status}`); } console.log(response.status, await response.json()); } finally { await env.delete(); } ``` Save the example as `first-request.mts` and run it: ```sh npx tsx first-request.mts ``` A successful run prints HTTP `200` and the Salesforce limits response. A response other than success throws an error, so the run does not pass by mistake. If startup fails or the wait times out, the `finally` block still deletes the environment. [Pause, resume, and delete environments](/environments/lifecycle/) covers the other cases. ## Continue with your integration Replace the limits request with a supported Salesforce request. Check `response.ok` before you treat a response as a success. The app returns its errors as HTTP responses. The SDK throws management API errors instead. Use [named apps](/environments/create/) when your test needs more than one Salesforce copy. Use [snapshots](/snapshots/reuse/) when each test needs the same starting records. For failures before the first request, follow [startup troubleshooting](/troubleshooting/startup/). --- # Resources and how they relate How organizations, workspaces, apps, actors, environments, and snapshots relate to one another. An environment is the unit you create for a task. It contains named apps and their data. The other resources decide who can use that environment, what it runs, and which data it starts with. ## A workspace scopes keys, not membership An organization is your company. Its members sign in to the dashboard. A workspace groups environments, snapshots, and API keys within that organization. A workspace API key limits a program to that workspace and the permissions on the key. Members belong to the organization and can see all of its workspaces. Two teams in different workspaces still share one membership. The [access guide](/environments/access/) explains the difference. ## An app name selects one copy of a fixed version An app is a replica product, such as `salesforce`. A version is a fixed build of that app. A fresh environment runs the app's default version. A snapshot keeps the versions and settings of its source apps. The app's name identifies one copy inside an environment. A short declaration such as `apps: ['salesforce']` also uses `salesforce` as that name. If your task needs two Salesforce copies, give them different names and connect by name. Each copy keeps its own data. See [creating named apps](/environments/create/). Settings configure the app when it starts. Records you write through its API are app data, not settings. Each replica decides which settings it accepts. The settings object does not let you set arbitrary vendor configuration. ## Members manage environments, actors make app requests A member is a person in your State Machines organization. An actor is an identity inside a replica app. Management permissions decide whether a member or API key can create, inspect, connect to, or delete an environment. After you connect, the selected actor is the identity of native app requests. A Salesforce administrator actor has no permission to manage State Machines workspaces. Discover the available actors from the environment's app version. Use the returned actor ID, not a role label or a State Machines member ID. [Credentials and actors](/environments/credentials/) explains the request headers and login information. ## A snapshot copies data into new environments An environment starts, runs, can pause, and ends deleted. Its status is `starting`, `running`, `paused`, `failed`, or `deleted`. A snapshot saves prepared app data for later environment creation. Restoring a snapshot creates independent apps and data. Writes to one restored environment do not change the snapshot or another restored environment. Use a snapshot when several tests need the same starting records. Pausing keeps the environment and its data for a limited time. Creating a snapshot saves reusable starting data. Deleting the environment removes its app data. A ready snapshot is a separate resource and stays. [Lifecycle](/environments/lifecycle/) and [snapshot reuse](/snapshots/reuse/) describe the status and cleanup rules. A State Machines snapshot is not an export of a customer's Salesforce organization. Its source is an existing State Machines environment. ## Requests and audit events record different things A request records an HTTP call to a replica app. An audit event records a management action or lifecycle outcome, such as an accepted environment creation or an environment that started. An accepted management action and a successful native app request prove different things. For example, `environment.created` does not prove that a later Salesforce insert succeeded. Use the native response and a read-back assertion for that. [Inspect requests and audit events](/environments/observability/) explains what is recorded and what can be left out. --- # Run an environment in the dashboard Select a workspace, create an environment, hand it to an agent, and inspect the result. Use the dashboard to create and inspect the same environments that your code manages through the SDK. Environments, snapshots, and API keys belong to one workspace, so select the workspace before you create resources. ## Create and connect Sign in at [app.usestatemachines.com](https://app.usestatemachines.com) and select the workspace. 1. Open **Environments**. 2. Choose **Create environment** and select the apps your task needs. For each app, choose who it acts as in **Act as**. Choose a **Time limit**. 3. Give the environment a name that identifies the task, then create it. 4. Open the environment and wait for **Running**. 5. To change the actor after creation, use **Acts as** in the app list. 6. Choose **Copy prompt**. If the environment has several apps, choose the app to hand to your agent. 7. Paste the prompt into the agent session that uses this environment. **Copy prompt** creates credentials for the selected actor and copies them with connection instructions. The prompt contains secrets. Share it only with the intended agent session, and keep it out of source files and logs. The app URL alone is not a credential. Every request also needs the actor token. The copied credentials expire. The prompt states when access ends. Copy a new prompt after that time. For your own client, use the SDK's `connect()` helper described in [credentials and actors](/environments/credentials/). An environment in **Starting** is accepted but not ready for app requests. If startup fails, read the failure shown for the affected app before you create another environment. ## Inspect a run Open **Requests** to inspect calls to the app. Select the time range, app, method, or status to narrow the list. Enter a search and press Enter to search recorded paths, query parameters, and text bodies. Clear the filters when a known call is missing. Open a request to inspect its status, query parameters, recorded headers, and body content. **Omitted** means the recorder did not store that body. A binary body has no text preview. Recording is best effort, so an empty request list does not prove that your client sent nothing. Open **Audit events** to see environment actions such as creation, pause, resume, and deletion. Audit events record lifecycle changes. They do not list app requests. See [requests and audit events](/environments/observability/) for the difference. ## Keep data or finish the task To take a break and keep the data, open **More actions** and choose **Pause**. Pausing releases compute and stops the time limit. Choose **Resume** to start it again before the next app request. Paused environments still expire, so check the deletion deadline on the page. To save reusable starting data from a running environment, open **More actions** and choose **Create snapshot**. Wait for **Ready** before you create an environment from the snapshot. When the task is complete, open **More actions** and choose **Delete**. Deleting removes the environment's app data. A saved snapshot is a separate resource and stays. [Environment lifecycle](/environments/lifecycle/) explains pause, deletion, and automatic expiry. If an action is missing or disabled, check the status and your permissions first. **Copy prompt** requires a running environment and `environments:connect`. **Pause**, **Resume**, and **Delete** require `environments:write`. **Create snapshot** requires `snapshots:write` and an active workspace. See [access troubleshooting](/troubleshooting/access/). --- # Give an agent a disposable environment Give an agent instructions, credentials, and a bounded create, connect, verify, and cleanup procedure. Give an agent access to a State Machines workspace when its task needs real HTTP calls to a replica app. The agent creates the environment, runs your integration, checks the results, and deletes the data afterward. Any agent that can read Markdown and run code can follow these instructions. ## Supply the instructions and the task Point the agent to the [agent skill](/skill.md) and the [documentation index](/llms.txt). In the dashboard, open **Getting started** and go to **Build with agents**. Its **Prompt** and **Skill** tabs hold the prompt and skill that ship with the SDK. The agent's project needs `@usestatemachines/sdk` 0.6.0 or later. The [quickstart](/get-started/quickstart/) covers the installation. The agent checks the version in `node_modules/@usestatemachines/sdk/package.json` and reads the installed README before it chooses methods. State the app behavior to test and the expected result. For example: ```text Test the Salesforce integration's account creation flow in State Machines. Create a disposable Salesforce environment, create an Account through the integration, and query it to verify the name. Report the environment ID, HTTP statuses, and assertion result. Delete the environment in a finally block. Do not print credentials or use a real Salesforce organization. ``` To hand over an environment that already runs, use the environment's **Copy prompt** in the dashboard. The prompt contains credentials for one app and one actor. See [Run an environment in the dashboard](/get-started/dashboard/). ## Provide access through the process Set `STATEMACHINES_API_KEY` in the process that runs the agent's code. Keep the key out of the prompt, generated source files, terminal output, and version control. The SDK does not load `.env` files. In a worker runtime, pass the key in the client configuration. The key's workspace and permissions decide which management operations the agent can run. The app actor decides the permissions of native app requests. See [workspace access](/environments/access/). ## Follow the agent procedure The agent follows these steps for each task. The [agent skill](/skill.md) carries the same procedure. ### Establish scope and prerequisites Read the user's task before taking action. Identify the integration to run, the required apps, the test data, the expected result, and whether cleanup includes snapshots. Create only the resources the task requires. Do not use an external customer's credentials or production data as a fallback. Check that `STATEMACHINES_API_KEY` is present in the process without displaying its value. Do not print environment variables, credential objects, login parameters, or request headers. Check the caller's [workspace and permissions](/environments/access/). `sm.me().limits` can be `null`, so handle that case. Do not raise permissions or change organization membership to make a test pass. ### Discover the app and the actor Use `sm.apps.list()` to find app IDs, and follow cursors when there are more pages. Connect by the app's name inside the environment, which can differ from its app ID. For an explicit actor, read that app's `versionId`. Page through `sm.apps.versions(appId)` until you find that exact version, and use an actor ID it declares. Do not pick the first version, the first app in an array, a member ID, or a role label. A Salesforce role name is not an actor ID. The [credentials guide](/environments/credentials/) includes a checked discovery example. ### Keep the environment ID before waiting Create with `{ wait: false }` to get the accepted environment ID. Enter a `try` block right away. Wait for `running`, connect, and run the task inside it. Await deletion in `finally`, including when the wait or an assertion fails. Pausing keeps data and is not cleanup. This first request shows the pattern: ```ts import { StateMachines } from '@usestatemachines/sdk'; const sm = new StateMachines(); const env = await sm.environments.create( { apps: ['salesforce'] }, { wait: false }, ); try { const running = await sm.environments.wait(env.id, { status: 'running' }); const salesforce = await running.connect('salesforce'); const response = await salesforce.fetch('/services/data/v67.0/limits'); if (!response.ok) { throw new Error(`Salesforce returned ${response.status}`); } console.log(response.status, await response.json()); } finally { await env.delete(); } ``` The create call itself can have an uncertain outcome. If the job must recover after a lost response or a restart, save the input and an explicit idempotency key before that call. Replay with the same input, key, workspace, and caller. A new key creates a different operation. Follow [operation recovery](/sdk-api/recovery/). ### Verify the integration's behavior Pass the returned app URL and credentials to the integration's configurable transport. Confirm that its requests go to that URL. A successful standalone SDK request does not prove that the integration was reconfigured. If the integration cannot take a different base URL or custom headers, fix that configuration first. Check `response.ok` or the exact status the test expects. For writes, read the stored result back and assert the relevant fields. For negative tests, assert the expected error code and that nothing else changed. Successful environment creation alone does not verify the integration. Use [Salesforce known differences](/apps/known-differences/) to limit what the result claims. Management calls and native app calls fail in different ways. Native `credentials.fetch()` returns HTTP failures as responses and never retries them. A gateway `503` does not mean you can send a write again. Read the `error.code` in the gateway response and check the app's stored state. If the write's outcome is uncertain, do not send it again. ### Finish or leave recoverable evidence A timeout or abort stops the caller's wait. It does not cancel accepted server work. Cleanup must use a signal that is not already aborted. If cleanup fails, keep the environment ID and report that failure separately from the test result. A snapshot in `saving` cannot be deleted yet. Keep its ID, wait for it to reach `ready` or `failed`, then delete it. Do not delete a reusable snapshot unless the task calls for it. See [snapshot creation](/snapshots/create/). ### Report the result Report each of these items: - The command that ran. - The app version and the SDK version. - The selected actor. - The resource IDs, without secrets. - The response statuses and the assertions. - The cleanup outcome. Say when a result was only typechecked or read, not executed. Do not claim production compatibility from one replica test. Deletion does not erase recorded management history, so do not claim that it does. ## Make the handoff recoverable When an operation can outlive an agent session, keep the environment ID and the create idempotency key in your job's private state. Keep them apart from credentials and out of a public report. A polling timeout or an interrupted agent does not prove that server work stopped. For parallel tests, give each task its own environment. Start each one from a ready snapshot instead of sharing one writable environment. See [isolated tests](/sdk-api/testing/) and [operation recovery](/sdk-api/recovery/). ## Choose the documentation format | Resource | Use it for | | --- | --- | | [Agent skill](/skill.md) | The procedure, credential handling, and cleanup rules | | [Documentation index](/llms.txt) | Finding the guides a task needs | | [Complete documentation](/llms-full.txt) | A tool that accepts one document. Focused pages use less context. | | The page's Markdown version | One full article, with the same examples as the website | | [OpenAPI contract](/openapi.json) | Management operation schemas. Vendor APIs have their own contracts. | These resources hold instructions and references. Reading them does not grant access to State Machines or create an environment. To run operations, the agent needs Node.js and the SDK or an HTTP client, an API key, and a task with a clear scope. --- # Create the apps your task needs Choose apps, assign names, and configure a disposable environment. Create an environment with the apps needed for one task. A string such as `'salesforce'` selects an app and also names that copy inside the environment. These examples use `@usestatemachines/sdk` 0.6.0 or later. Install it with `npm install @usestatemachines/sdk`. ## Discover apps before selecting them Call `sm.apps.list()` to read the available app IDs. Follow `nextCursor` if the response has another page. Call `sm.apps.versions(appId)` to read published versions, their APIs, and their actors. For creation with `apps`, a new environment uses each app's default version. Version metadata includes `isDefault`, `status`, `apis`, and `actors`. The list includes retired versions. You can read versions, but you cannot choose one when you create an environment. An app without a default version fails creation with `version_unavailable`. ## Name repeated apps Use an object when an app needs its own name or settings. Names identify copies within the environment and must be unique. Repeating the string `'salesforce'` twice produces two identical names and fails validation. An app name is 1 to 40 characters. It starts with a lowercase letter and contains only lowercase letters, digits, and hyphens. An environment holds at most 10 apps. Pass the app name to `connect()`, not the underlying app ID. This example creates two Salesforce copies and connects to `partner-org`: ```ts import { StateMachines } from '@usestatemachines/sdk'; const sm = new StateMachines(); const env = await sm.environments.create( { name: 'CRM comparison', labels: { suite: 'integration' }, apps: ['salesforce', { app: 'salesforce', name: 'partner-org' }], }, { wait: false }, ); try { await sm.environments.wait(env.id, { status: 'running' }); const partner = await env.connect('partner-org'); const response = await partner.fetch('/services/data/v67.0/limits'); if (!response.ok) { throw new Error(`Salesforce returned ${response.status}`); } console.log(response.status); } finally { await env.delete(); } ``` The example accepts creation without waiting, then waits inside `try`. Accepting first keeps the environment ID in hand, so `finally` can delete the environment if waiting fails. ## Choose an actor `connect()` uses the app's default actor unless you pass `actorId`. To connect as another actor: 1. Find the version the named app runs. The helper below reads the environment's `versionId` for that app and follows pagination through `sm.apps.versions(appId)`. 2. Read the returned `actors` and `apis`. `isDefault` describes the current default version, which can differ from the version the environment runs. 3. Pass the chosen actor's `id` to `env.connect(appName, { actorId })`. ```ts import type { StateMachines, Version } from '@usestatemachines/sdk'; export async function findRunningVersion( sm: StateMachines, environmentId: string, appName: string, ): Promise { const env = await sm.environments.get(environmentId); const app = env.apps.find((entry) => entry.name === appName); if (!app) { throw new Error(`Environment has no app named ${appName}`); } let cursor: string | undefined; do { const page = await sm.apps.versions(app.appId, { cursor }); const version = page.data.find((entry) => entry.id === app.versionId); if (version) { return version; } cursor = page.nextCursor ?? undefined; } while (cursor !== undefined); throw new Error(`Version ${app.versionId} was not found`); } ``` Reading versions does not switch the environment to another version. [Credentials and actors](/environments/credentials/#actors) describes actor IDs and permissions. ## Set task metadata and settings Use `name` for a readable task name and `labels` for string metadata such as a test suite or CI run ID. Set `timeLimitMinutes` when the task needs a specific time limit. `sm.me().limits.timeLimitMinutes` returns the default and the maximum. [Pause, resume, and delete environments](/environments/lifecycle/#deleting-ends-ownership) describes what the time limit counts. State Machines passes app `settings` unchanged to the selected app. Salesforce accepts its documented setup and settings shape. Arbitrary Salesforce metadata is not an environment setting. A rejected value produces an `invalid_settings` startup failure. See [Salesforce](/apps/salesforce/). ## Start with existing data To reuse prepared data, supply `snapshotId` instead of `apps`. A snapshot determines the apps and saved data of the new environment. Follow [creating environments from snapshots](/snapshots/reuse/). ## Optional create fields Supply exactly one of `apps` and `snapshotId`. For `apps`, each entry is an app ID or `{ app, name?, settings? }`. The TypeScript type does not enforce that rule. The API checks it. The remaining fields are optional: | Field | Effect | | --- | --- | | `workspaceId` | Required for a member token unless the `StateMachines` constructor sets `workspaceId`. An API key uses its own workspace. Naming any other workspace with an API key returns `not_found`. | | `name` | Human-readable environment name, or null. It is separate from app names. | | `labels` | String-to-string metadata map. An absent map starts empty. | | `timeLimitMinutes` | Time limit in minutes. If omitted, the environment uses the default from `sm.me().limits.timeLimitMinutes`. | | `tokenInUrl` | When true, credentials include environment authentication in the app URL path. Defaults to false. | There is no create field for a pinned version. The accepted environment's `apps` contain the actual `versionId`, version label, name, and settings for each copy. Store those values with a test result when you need to reproduce it. A snapshot keeps its source versions and settings for later copies. Creation requires `environments:write`. Creating from a snapshot also requires `snapshots:read`. An archived workspace refuses new environments. The concurrent limit counts every `starting` and `running` environment in the organization, across all workspaces. `sm.me().limits.concurrentEnvironments` returns it. Past the limit, creation fails with `environment_limit_reached`. Pause or delete an environment to free a place. Read existing environments and their creators before you decide what to delete. ## Update task metadata `sm.environments.update(id, { name, labels })` changes metadata on an existing environment. Omit a field to leave it unchanged. Updating `labels` replaces the entire map, so include every label you want to keep. Use `name: null` to clear the name or `labels: {}` to clear labels. Deleted environments reject metadata updates. This call cannot change app selection, settings, the time limit, or `tokenInUrl`. Create another environment when the task needs a different configuration. Names and labels are descriptive metadata, so keep credentials and customer secrets out of both. For the complete field shapes and validation constraints, see the [endpoint reference](/sdk-api/endpoints/). For accepted operations whose outcome is uncertain, use [idempotent recovery](/sdk-api/recovery/). --- # Pause, resume, and delete environments Pause, resume, and delete an environment, and read its status, time limit, and lifecycle timestamps. An environment exists as soon as State Machines accepts its creation. Its status describes what it can do now. An accepted environment is not yet ready for app requests. ## Status and data | Status | Meaning | | --- | --- | | `starting` | The apps are starting. Wait before sending app requests. | | `running` | The apps can receive requests. | | `paused` | Compute is released and data remains. Resume before making requests. | | `failed` | An environment that previously ran could not start or restart. Its data remains available for a resume attempt. It is deleted one day after it failed. | | `deleted` | The environment has ended and data cleanup proceeds. Its metadata remains readable by ID. | If an environment fails before it ever runs, State Machines deletes it with reason `failed`. The SDK reports that through `EnvironmentFailedError`. ## Pausing keeps the current data `pause()` changes the status when the API answers. From then on, the gateway answers app requests with `environment_not_running`. Pausing releases compute and stops the time limit. The environment keeps its data. Pausing a `starting` environment fails with `invalid_status`. `resume()` starts apps on that same data. The SDK waits for `running` unless you pass `{ wait: false }`. Resuming counts against the organization's concurrent limit and can fail with `environment_limit_reached`. A returned environment object describes one read of the resource. To get its new status, use the object returned by the operation or call `refresh()`. ```ts import { StateMachines } from '@usestatemachines/sdk'; const sm = new StateMachines(); const env = await sm.environments.create( { apps: ['salesforce'] }, { wait: false }, ); try { await sm.environments.wait(env.id, { status: 'running' }); const paused = await env.pause(); console.log(paused.status); const resumed = await env.resume(); console.log(resumed.status); const salesforce = await resumed.connect('salesforce'); const response = await salesforce.fetch('/services/data/v67.0/limits'); if (!response.ok) { throw new Error(`Salesforce returned ${response.status}`); } console.log(response.status); } finally { await env.delete(); } ``` ## Deleting ends ownership `delete()` marks the environment deleted and refuses further app access when the API answers. Compute and data cleanup continue after the response. Repeating `delete()` is safe, so call it in an awaited `finally` block. Pause for an interactive break, but delete the environment at the end of a task. The time limit counts time spent in `starting` and `running`. `sm.me().limits.timeLimitMinutes` returns its default and maximum. State Machines deletes the environment when the time limit ends. State Machines deletes a paused or failed environment one day after it paused or failed, with reason `expired`. `expiresAt` shows that deadline. A ready snapshot is a separate saved copy. Use [snapshots](/snapshots/create/) for reusable data that must outlive the source environment. ## Read lifecycle timestamps `createdAt` records acceptance. `startedAt` records the first time the environment became running. A resume does not change it. `pausedAt` records when the current paused or failed period began. `expiresAt` is the current deletion deadline and is null once deleted. A deleted environment's `reason` distinguishes expiration, failure before first startup, and cancellation by staff. A caller-requested deletion has a null reason. If `failure` is present, it names the affected app and gives its code and message. Pause, resume, and delete are actions. `wait()` only observes. Waiting for `running` on a paused environment does not resume it and fails with `UnexpectedStatusError`. When a resumed app fails, `EnvironmentFailedError` carries the environment for diagnosis. See [SDK errors](/sdk-api/errors/) for polling outcomes. ## Cleanup belongs to the caller An accepted environment keeps running after the caller stops waiting or loses its process. Get the ID with `wait: false` before the work that needs cleanup. Keep an explicit create key when the acceptance response itself could be lost. An awaited `finally` handles failures while the process is alive. CI cancellation or a runtime crash can prevent it from running. Persist owned environment IDs with the job, use labels to identify the run, and recover cleanup in a later job. The time limit is a final bound, not confirmation that cleanup succeeded. If deletion fails, keep both the environment ID and the cleanup error, then retry the deletion. Do not reuse an aborted work signal for cleanup, because it cancels the delete request too. --- # Credentials and actors Authenticate native app requests without sending your State Machines API key to the app. `env.connect(appName, { actorId? })` returns credentials for one app in an environment. The app name is the name assigned at creation, which defaults to the app ID. ## Credential fields | Field | Purpose | | --- | --- | | `environmentId`, `app`, `actorId` | Identify the environment, named app, and actor these credentials belong to. | | `url` | Base URL for this app in the environment. | | `headers` | Environment authentication headers, normally `StateMachines-Environment-Token`. | | `token` | Token for the actor inside the app. | | `expiresAt` | The environment expiry at credential creation, represented as a `Date` in the SDK. | | `login` | A nullable object with `authentication`, `url`, `parameters`, and `secrets` for the vendor login call. | | `fetch(path, init?)` | SDK helper that calls a path under the app URL with the required credentials. | Credentials contain secrets. State Machines does not store the returned credentials. Never log tokens, headers, or URLs that contain a token. ## Actors An actor is a predefined identity inside the app. Omitting `actorId` uses the app's default actor. Salesforce's default is its administrator. The app's version declares the available actors. `sm.apps.versions(appId)` returns them. A Salesforce actor ID is a Salesforce user ID, not a role name. [Choose an actor](/environments/create/#choose-an-actor) shows how to find the actors of the version an environment runs. Actor permissions govern native app operations. State Machines workspace permissions separately govern environment management and the ability to request credentials. ## Request behavior `credentials.fetch()` adds the environment headers and selects actor authentication for the matching API path. Salesforce REST and Bulk 2.0 use `Authorization: Bearer`. `credentials.fetch()` does not authenticate SOAP calls. Put `credentials.token` in `SessionHeader/sessionId` in the envelope yourself. The helper never adds your State Machines API key. It does not follow redirects or retry requests. Caller-provided headers cannot replace environment authentication or the selected actor header. A leading slash resolves against the app root. The helper returns HTTP error responses without throwing a management error. A network failure can still reject the call. Check `response.ok` and inspect the matching [error format](/troubleshooting/requests/). When an environment is created with `tokenInUrl: true`, the URL carries the environment token and `headers` is empty. Treat that URL as a secret. A separate vendor client must send credentials only to this app URL and disable redirects. ## Credential lifetime and failures Requesting credentials does not prove readiness. The API issues credentials for a starting, paused, or failed environment. It refuses deleted environments. Wait for `running` before native app calls. Pausing or deleting the environment makes app requests fail even when a caller still holds the credential object. After resume, request credentials again and use the current returned URL and expiry. `connect()` requests credentials and reads the matching version's API definitions. A metadata-read failure can reject `connect()` even when the credential request succeeded. The SDK caches successful API definitions per version on the client. It does not cache credential objects. The fetch helper reaches only paths under the returned app URL. It rejects a path that escapes the app URL, including another origin, protocol-relative paths, and encoded traversal. It throws `TypeError` before sending those requests. Supply a vendor path such as `/services/data/v67.0/limits`, not a replacement gateway URL. Native requests accept standard `RequestInit`, including `signal`, method, body, and headers. They do not use the client's `attemptTimeoutMs`. Pass a `signal` when your integration needs a deadline. The SDK uses a custom constructor `fetch` implementation for both management and native calls, so that implementation must not attach the management key to every request. --- # Workspaces, members, and API keys Understand organization membership, workspace scope, and the permissions that separate management from app access. State Machines uses separate identities for people in your organization, programs that manage environments, and actors inside replica apps. ## Membership belongs to the organization An organization represents your company. Each member has the role `owner`, `admin`, or `member`. A role grants a set of management permissions. To find out why an action was denied, read `sm.me().permissions`. In State Machines today, all three roles hold every permission. The roles differ only in who can change whom: nobody changes a member ranked above them, so only an owner changes an owner. An invitation invites a person to join the organization. After accepting, the person can see every workspace in that organization. There is no separate workspace membership list. Workspaces group resources. They do not restrict one organization member to a private set of environments. Member management requires a member session and the matching `members:read` or `members:write` permission. A person cannot change their own role, remove themselves, grant a role above their own, or change someone ranked above them. The organization must keep at least one owner. ## Workspace selection controls resource scope A workspace groups environments, snapshots, and API keys inside the organization. The dashboard's workspace selector controls the workspace you are viewing. Switching the selector does not move resources or change an existing API key's scope. For member access tokens, the SDK's `workspaceId` selects a workspace for calls that do not otherwise identify one. Resource IDs identify their own workspace on resource-specific calls. A workspace API key always remains bound to its workspace. Archiving a workspace prevents new environments and snapshots, and changes to their metadata. Existing resources remain readable. Environment pause, resume, and deletion remain available with the required permissions. Archiving does not revoke API keys or delete environments. ## API keys identify programs An API key belongs to one workspace. It can hold these permissions: | Permission | Allows | | --- | --- | | `environments:read` | Read environments, recorded requests and bodies, and audit events. | | `environments:write` | Create, update, pause, resume, and delete environments. | | `environments:connect` | Obtain app credentials. | | `snapshots:read` | Read snapshots and use them to create environments, with `environments:write`. | | `snapshots:write` | Create, update, and delete snapshots. | These permissions are separate. Write permission does not imply read or connect permission. A workflow that creates an environment, waits for it, and connects needs the permission for each operation. A key cannot receive permissions its creator does not hold. API keys cannot manage members or invitations, create or modify workspaces, or list, create, or revoke API keys. Those operations require a member access token and the matching permission. Passing a workspace ID or more permission names does not make a key a member session. The dashboard's **Create API key** form requests the key-eligible permissions held by the signed-in member. To request a smaller set, use the member-authenticated management API. Create a key in the workspace it needs and store its value when shown. The dashboard cannot show the complete value again. Replaying a successful key creation with the same idempotency key also returns metadata without the secret. If the value is lost, revoke that key and create a replacement. The SDK reads `STATEMACHINES_API_KEY` by default in any runtime with a global `process`, such as Node.js. `sm.me()` returns the current principal, its permissions, its organization and role, the workspace an API key is bound to (null for a member), and the organization's limits. It never returns the secret. ## Revocation and app access have different effects Revoking an API key prevents future management calls with that key. Environments created by that key continue until deleted or expired. Credentials already issued for their apps keep working. Delete the environment when its app data and access must end. Removing a member revokes every API key that member created, in every workspace. [Credentials and actors](/environments/credentials/) describes actors, the identities inside an app. [Access troubleshooting](/troubleshooting/access/) separates missing authentication, insufficient permissions, and a resource outside the caller's workspace. --- # Requests and audit events Interpret recorded app traffic, body coverage, statistics, and audit events. Requests record calls through the app gateway. Audit events record State Machines resource actions. Both require `environments:read`, including access to stored request and response bodies. API keys can read records only in their own workspace. ## Recorded requests `sm.requests.list({ environmentId })` returns recorded calls, newest first. Each record identifies the app, method, path, query, status, duration, and timestamps. `respondedBy` identifies `app` or `gateway`. `errorCode` is present when the gateway answered with an error. The gateway does not guarantee that it records every request. Recorder capacity or storage failures can leave gaps. Some failures occur before an authenticated request can be recorded. Neither the list nor its statistics is a complete transaction ledger. The request list accepts these filters: | Filter | Meaning | | --- | --- | | `environmentId` | Required environment ID. | | `app` | App name within the environment. | | `method` | Exact HTTP method. | | `responseStatus` | Exact HTTP response status. | | `statusClass` | `2xx`, `3xx`, `4xx`, or `5xx`. | | `writesOnly` | Methods `POST`, `PUT`, `PATCH`, and `DELETE`. A match does not show that a write succeeded. | | `search` | Case-insensitive text search in the path, query, and recorded searchable text bodies. Requires `from` and `to`. | | `from`, `to` | SDK `Date` values supplied together, with `to` after `from`. The start is inclusive and the end exclusive. | A window can span at most 7 days. The API rejects an invalid window with `invalid_request`. Narrow the window to the failing run. Filters combine, so an exact status outside the chosen status class produces no results. A page cursor belongs to its original filters. After changing filters, request a new first page. Omitted bodies and redacted values cannot be recovered through search. ## Request details and bodies `sm.requests.get(id)` adds selected headers and body metadata. The request and response have separate coverage values: | Coverage | Meaning | | --- | --- | | `recorded` | The recorder stored the body. | | `redacted` | The stored body replaces recognized sensitive fields. | | `omitted` | No body was stored. `reason` explains why when it is known. | `encoding` is `utf8` or `binary`. A binary body has no text preview. `previewTruncated` means the preview holds only the first 8 KiB of the stored body. The dashboard fetches the remaining stored text when it opens a truncated text body. `sm.requests.body(id, 'request')` and `sm.requests.body(id, 'response')` return stored bytes as `Uint8Array`. The result preserves recording redactions. An omitted body returns `not_found`, even when the request record exists. Downloads cannot restore omitted content or credentials. The recorder stores selected headers, not every original header. It omits authentication headers and cookies. See [credentials and recorded data](/environments/security/) for handling guidance. ## Request statistics `sm.requests.stats({ environmentId, from, to })` requires SDK `Date` values for both ends of the window. It accepts the same app, method, status, write, and search filters as the list. The response includes counts, latency percentiles, and time buckets for recorded calls. `successCount` includes statuses below `400`, including redirects. These HTTP outcomes do not establish whether a business operation succeeded. Latency percentiles are null when there are no matching calls. ```ts import { type StateMachines, StateMachinesError } from '@usestatemachines/sdk'; export async function inspectRecordedRequests( sm: StateMachines, environmentId: string, from: Date, to: Date, ) { const filters = { environmentId, from, to }; const stats = await sm.requests.stats(filters); console.log('Recorded requests:', stats.count); let cursor: string | undefined; do { const page = await sm.requests.list({ ...filters, cursor }); for (const request of page.data) { const detail = await sm.requests.get(request.id); console.log(detail.id, detail.responseStatus, detail.respondedBy); if (detail.responseBody.coverage === 'omitted') { continue; } try { const bytes = await sm.requests.body(detail.id, 'response'); console.log('Stored response bytes:', bytes.byteLength); } catch (error) { if ( !(error instanceof StateMachinesError) || error.code !== 'not_found' ) { throw error; } console.log('No stored response body:', detail.id); } } cursor = page.nextCursor ?? undefined; } while (cursor !== undefined); } ``` ## Audit events `sm.auditEvents.list({ environmentId })` returns the environment's audit events. `sm.auditEvents.list({ workspaceId })` returns the workspace's audit events. An API key can omit `workspaceId` to read its own workspace's audit events. If both IDs are supplied, the environment must belong to that workspace. Events include a name, principal, subject ID, occurrence time, event data, and a management request ID when one exists. The principal identifies a member, API key, or the system. Examples include `environment.paused`, `environment.deleted`, `credentials.created`, and `snapshot.ready`. An environment lifecycle event can explain why app requests stopped. A `credentials.created` event records issuance, not every later use of those credentials. A management request ID links events to a management call. It is not an app request record ID. Deleting an environment removes its app data. Recorded requests and audit events are kept separately from the environment. Recorded bodies can contain test data after the environment ends. These records are not a replacement for a saved snapshot or an archive with a promised retention period. --- # Handle credentials and recorded data Choose scoped credentials, protect agent handoffs, and account for copies of app data. Keep management keys, app credentials, and recorded app data separate when deciding who needs access. `environments:read` permits reading recorded bodies. `environments:connect` permits obtaining credentials that act inside an app. ## Give a program the scope it needs Create the program's API key in the workspace that holds its environments. When creating a key through the management API, request only the permissions the program needs. The creator must hold each requested permission. [Workspace access](/environments/access/) describes the available permissions and member-only operations. Use an app actor whose permissions match the test. An administrator actor can prepare a fixture, but it can hide permission failures that another actor would encounter. Store keys and credentials in the process environment or your secret store. Exclude them from source files, logs, screenshots, and shared exports. Treat **Copy prompt** output as a secret because it contains credentials for the selected app actor. A URL created with `tokenInUrl: true` is also a secret. For an exposed management key, revoke it and replace the value used by the program. If app credentials were exposed, delete the affected environment to end app access. Revoking the management key does not invalidate previously issued app credentials. ## Check recorded data before sharing it Open a request's coverage and omission reason before using it as evidence. The recorder replaces recognized credential fields with `[REDACTED]`. It omits any body that contains a credential State Machines issued. Application records can still contain sensitive values under other names. The gateway omits bodies for credential routes and for formats it cannot redact, including XML and multipart payloads. The recorder also omits a JSON or form body that is invalid or larger than 1 MiB. These controls do not remove arbitrary personal or business data from ordinary records. Use synthetic data for fixtures. Before sharing a request, downloaded body, or agent transcript, remove sensitive application data yourself. Preserve the environment ID, app version, request method, status, and sanitized error needed to reproduce the issue. ## Clean up each copy of the data Delete the environment when its task ends. Delete unneeded snapshots separately because snapshots can outlive their source environment. An environment already restored from a snapshot owns another copy of the app data. Recorded requests and audit events are kept separately from the environment. Deleting an environment does not delete those records. The API has no operation that erases a single request. State Machines publishes no retention period for recorded requests and audit events. Do not put data that must be deleted on a schedule into an environment. Remove local body downloads and copied credentials from the places where you stored them. Deleting a State Machines resource cannot remove files or transcripts outside State Machines. --- # Save prepared data in a snapshot Save all apps in a running environment as reusable starting data. Create a snapshot after preparing the records your tests need. A snapshot saves the data of every app in one running State Machines environment at one moment. A snapshot does not import a customer's Salesforce organization. Its source is an environment you already created and populated in State Machines. ## Prepare and save the data 1. Create an environment and wait until it is `running`. 2. Write the records needed by the scenario through the app's native API. 3. Check that each write succeeded before saving a snapshot. 4. Call `env.snapshots.create({ name: 'Account fixture' })`. 5. Keep the returned snapshot ID to create environments from it. The SDK waits for `ready` by default. Passing `{ wait: false }` returns after the save is accepted, with status `saving`. Use `sm.snapshots.wait(id, { status: 'ready' })` before restoring it. If the save fails, the wait throws `SnapshotFailedError`. ## Run a complete example This example writes an Account, saves a snapshot, creates another environment from it, and queries the restored record. It deletes both environments in awaited cleanup and deletes the demonstration snapshot after saving ends. If a wait ends while the snapshot is still saving, the example reports the snapshot ID for later cleanup instead of calling delete, which would fail. ```ts import { StateMachines } from '@usestatemachines/sdk'; const sm = new StateMachines(); const source = await sm.environments.create( { apps: ['salesforce'] }, { wait: false }, ); try { await sm.environments.wait(source.id, { status: 'running' }); const salesforce = await source.connect('salesforce'); const created = await salesforce.fetch( '/services/data/v67.0/sobjects/Account', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ Name: 'Snapshot example' }), }, ); if (!created.ok) { throw new Error(`Account creation returned ${created.status}`); } const snapshot = await source.snapshots.create( { name: 'Account fixture' }, { wait: false }, ); console.log('Snapshot ID for cleanup:', snapshot.id); try { await sm.snapshots.wait(snapshot.id, { status: 'ready' }); const copy = await sm.environments.create( { snapshotId: snapshot.id }, { wait: false }, ); try { await sm.environments.wait(copy.id, { status: 'running' }); const restored = await copy.connect('salesforce'); const query = encodeURIComponent( "SELECT Id, Name FROM Account WHERE Name = 'Snapshot example'", ); const response = await restored.fetch( `/services/data/v67.0/query?q=${query}`, ); if (!response.ok) { throw new Error(`Query returned ${response.status}`); } console.log(await response.json()); } finally { await copy.delete(); } } finally { const current = await sm.snapshots.get(snapshot.id); if (current.status === 'saving') { console.error('Save still pending; retry snapshot cleanup:', snapshot.id); } else { await sm.snapshots.delete(snapshot.id); } } } finally { await source.delete(); } ``` The example obtains the snapshot ID with `wait: false` before waiting. If the save is still pending, deleting the source interrupts it. Read the snapshot again and delete it after it reaches `failed` or `ready`. Preserve this ID in job storage when cleanup must survive process exit. The example uses `@usestatemachines/sdk` 0.6.0 or later. For a reusable fixture, keep the ready snapshot and record its ID instead of deleting it after the demonstration. ## Keep the source available until ready Only one snapshot can be `saving` for an environment at a time. A concurrent attempt fails with `snapshot_in_progress`. List snapshots for that environment, wait for the active save to finish, then decide whether you need another snapshot. Pausing or deleting the environment before the save completes fails the snapshot with code `interrupted`. A failed snapshot cannot start an environment. Inspect the failure and create another snapshot while the source is running. A ready snapshot is independent of the source environment's running lifetime. Delete the source when preparation is complete. Follow [reusing snapshots](/snapshots/reuse/) to start isolated copies of the prepared data. ## Recover an interrupted save request Snapshot creation accepts `idempotencyKey`, `wait`, `timeoutMs`, and `signal`. The SDK creates a key for its own automatic retries. To recover across separate calls or process restarts, persist your own key with `{ environmentId, name }` before creation. A timeout does not cancel the save. Keep the source running if you want saving to finish, then call `sm.snapshots.wait(id, { status: 'ready' })` again. With `wait: false`, `timeoutMs` does not apply. A failed snapshot carries `failure.code` and `failure.message`. The failure codes are `interrupted` and `internal_error`. To create a snapshot, you need `snapshots:write` and access to the source environment's workspace. The source must be `running`, and its workspace must not be archived (`workspace_archived`). The snapshot takes its workspace from the source. It does not accept a separate app list, actor, or target workspace. After a snapshot is ready, its saved data is fixed. `update()` changes only its name. To change the data, see [Refresh a fixture](/snapshots/reuse/#refresh-a-fixture). --- # Start tests from the same snapshot Create independent environments from saved data and manage the fixture lifecycle. Use a ready snapshot when each test needs the same prepared records. Every environment created from it gets its own copy of the saved app data. Writes in one restored environment do not change the snapshot or another environment's data. ## Select a ready snapshot Find the snapshot in the dashboard or call `sm.snapshots.list()`. Lists are paginated. Filter by the source environment or status when you know them, and follow `nextCursor` for the remaining results. Read the snapshot by ID before a run if you do not know its status. Only status `ready` can start a new environment. A snapshot in `saving` needs more time. A failed or deleted snapshot needs a replacement. Rename a snapshot with `sm.snapshots.update(id, { name })`. Pass `name: null` to clear the name. A rename does not change its ID or data. Use names to describe fixture intent, such as an account with an open opportunity. Keep the snapshot ID in your test configuration so a renamed snapshot does not change which fixture your run selects. ## Create an isolated copy Call `sm.environments.create({ snapshotId })` in place of a create call with `apps`. The snapshot supplies the saved app names, versions, settings, and data, even if the current default version has changed. Creation does not accept app overrides alongside `snapshotId`. The saved versions stay published while a snapshot that is not deleted uses them, so a ready snapshot keeps starting environments after a newer default version is published. The caller needs `environments:write` and `snapshots:read`. The snapshot must be in the new environment's workspace. Wait for `running`, request credentials, run the test, and delete it in `finally`. The [complete snapshot example](/snapshots/create/#run-a-complete-example) shows creation, restoration, verification, and cleanup together. The [test helper](/sdk-api/testing/) shows how to keep cleanup with the code that creates an environment. ## Refresh a fixture To change a fixture, create an environment from the existing snapshot, write the new records, and create a new snapshot. Update the test configuration to the new snapshot ID after checking the resulting data. Existing snapshots do not change when restored environments are edited. If a test sees different records than expected, check the snapshot ID, app name, and actor before changing its query. See [data troubleshooting](/troubleshooting/data/). ## Inspect the saved fixture `sm.snapshots.get(id)` returns the source `environmentId`, workspace, status, name, creator, and timestamps. `apps` identifies each saved app by its name, `appId`, version, and `versionId`. `sizeBytes` on the snapshot and its apps is null until the size is known. `createdAt` records acceptance. `readyAt` records when the save finished. A failed snapshot has a failure code and message. Creating an environment from it fails with `invalid_status`. Delete a ready or failed snapshot with `sm.snapshots.delete(id)` when no future runs need it. Deletion marks the snapshot deleted and prevents new environments from using it. Environments already created from it keep their own data. File cleanup can finish later. A `saving` snapshot rejects deletion with `invalid_status`. Wait until the save is ready or failed before you delete it. Snapshot listing omits deleted entries unless the status filter includes them. --- # Salesforce API reference Supported protocols, authentication, app settings, and compatibility boundaries for the Salesforce replica. The `salesforce` app implements Salesforce APIs over isolated app data. An app version supplies a predefined catalog, configuration records, and record rules. Creating an environment does not connect to a customer's Salesforce organization or import its metadata. ## Protocols and operations Paths below use `{version}` for a supported Salesforce API version. This differs from the State Machines app version, which identifies the replica release. | API | Paths and operations | | --- | --- | | REST discovery | `GET /services/data`, `GET /services/data/v{version}`, global describe at `/sobjects`, and object describe at `/sobjects/{object}/describe`. | | REST records | Create, retrieve, update, and delete records by ID. Retrieve and upsert by external ID. Conditional reads and writes. | | REST query | `/query`, `/queryAll`, and query locator pages. `DELETE /query/{locator}` returns HTTP `405` (`METHOD_NOT_ALLOWED`) and leaves the locator usable. | | REST composite | `/composite`, `/composite/batch`, `/composite/graph`, `/composite/tree/{object}`, and sObject Collections. | | REST limits and replication | `/limits`, `/limits/recordCount`, and each object's `/updated` and `/deleted` resources. | | Bulk API 2.0 | Ingest and query jobs under `/services/data/v{version}/jobs`, CSV uploads, job state, and result retrieval. | | SOAP Partner | `/services/Soap/u/{version}`, optionally followed by an organization ID. Supported operations are listed below. | | OAuth | Token, revoke, and userinfo endpoints under `/services/oauth2`, plus `/id/{organization}/{user}`. | | Tooling | Create, read, and delete `WorkflowOutboundMessage`. General Tooling API support is absent. | REST paths after the discovery row are relative to `/services/data/v{version}`. The resource directory lists a subset of the registered routes. Its omission of composite or Tooling does not mean those routes are absent. The app version's `apis` declares protocol compatibility. Retrieve versions with `sm.apps.versions('salesforce')` and match the environment's selected app version. Each API entry lists its `apiVersions` and `limitations`. Native REST discovery lists served API versions. REST, SOAP Partner, and the identity URL serve API versions 31.0 to 67.0. Bulk API 2.0 serves 47.0 to 67.0. A REST request below 31.0 returns HTTP `410` (`GONE`), and any other unserved version returns `404` (`NOT_FOUND`). Version 67.0 is the seed version. The replica's catalog and behavior were recorded from a Salesforce organization at that version, and older versions are projections of it. ## Authentication and actors `env.connect('salesforce')` selects the default administrator actor. If you assigned another name to the app at creation, pass that name instead. Pass an `actorId` from the selected app version to use another available actor. Object access, field access, record sharing, and API permissions depend on the actor. An administrator's successful request does not establish that another actor can make it. SOAP requires the actor token in the envelope's `SessionHeader/sessionId`. See [credentials and actors](/environments/credentials/) for the headers every app request needs. ## SOAP Partner operations The SOAP adapter supports these operations: - Sessions and identity: `login`, `logout`, `getUserInfo`, and `getServerTimestamp`. - Discovery: `describeGlobal`, `describeSObject`, and `describeSObjects`. - Records: `create`, `update`, `upsert`, `delete`, `undelete`, and `retrieve`. - Queries: `query`, `queryAll`, and `queryMore`. - Replication: `getDeleted` and `getUpdated`. `merge` and `convertLead` return `INVALID_OPERATION`. The Enterprise SOAP API is unsupported. SOAP `login()` returns `INVALID_OPERATION` at API version 65.0 and later. From 31.0 to 64.0 it requires `settings.soapApiLoginEnabled: true`, which is off by default, and a user with the Use Any API Auth permission. Other SOAP operations accept the actor token from `connect()` in `SessionHeader/sessionId` without calling `login()`. ## OAuth operations The token endpoint implements `password`, `refresh_token`, `client_credentials`, and JWT bearer grants. Grant success depends on the connected app and credentials supplied to the replica. A client-credentials grant requires a configured run-as user. JWT bearer requires a valid assertion signed for the configured connected app. These protocol implementations do not imply that every environment supplies credentials for every grant. The revoke endpoint accepts access or refresh tokens. The userinfo and identity endpoints return information for the authenticated user. The replica does not provide a browser authorization-code flow. ## App settings The public Salesforce app settings accept the following structure. These keys belong to the Salesforce app's settings, not the top-level environment settings. | Key | Accepted configuration | | --- | --- | | `preset` | `development`, also used when omitted. | | `configure.organization` | `target: { $ref: 'defaults.organization' }` and `values: { Name: string }`. | | `configure.administrator` | `target: { $ref: 'defaults.administrator' }` and `values: { Username: string }`. | | `settings.soapApiLoginEnabled` | A boolean. | The validator rejects unknown keys and invalid values. Names and usernames must be nonempty strings of at most 80 characters. Startup reports rejected settings as `invalid_settings`. These settings customize predefined records and the SOAP login policy. They do not define custom objects, custom fields, packages, formulas, Flow definitions, or Apex code. ## Request limits and error formats The replica counts `DailyApiRequests` over a rolling 24-hour window and reports it in `/limits` and in the `Sforce-Limit-Info` response header. It does not refuse requests over the limit. Every other limit reports `Remaining` equal to `Max`. These values do not represent a customer's Salesforce subscription. State Machines accepts request bodies up to 20 MiB and returns responses up to 4 MiB. REST errors use arrays with `message`, `errorCode`, and sometimes `fields`. Bulk errors use arrays with `errorCode` and `message`. SOAP returns a `soapenv:Fault`. OAuth uses `error` and `error_description`. See [request troubleshooting](/troubleshooting/requests/) for the separate State Machines error formats. For task instructions, see [records and schema](/apps/salesforce-records/), [SOQL queries](/apps/salesforce-query/), and [Bulk jobs](/apps/salesforce-bulk/). Read [known differences](/apps/known-differences/) before choosing compatibility assertions. --- # Discover schema and work with records Inspect actor-visible objects and fields, create related records, and choose a composite request. Use this guide to prepare Salesforce test data and verify record writes. First [connect to the environment](/environments/credentials/) with the actor your integration uses. The HTTP excerpts show paths and bodies. Send them through your authenticated client at the returned app URL. Replace `{version}` with a served API version and `{recordId}` with an ID returned by a previous request. ## Discover the schema for your actor 1. Request `GET /services/data`. Select a version from the response and confirm that the app version supports the protocol you need. 2. Request `GET /services/data/v{version}/sobjects`. Find the object's API name, such as `Account`, and inspect its operation flags. 3. Request `GET /services/data/v{version}/sobjects/Account/describe`. Inspect the fields and relationships before building a payload. Use API names in requests. Check `createable` and `updateable` on both the object and its fields. For create payloads, check `nillable`, `defaultedOnCreate`, and field defaults. Inspect `picklistValues` for choices, and `referenceTo` for the objects a relationship field accepts. Describe reflects the selected actor and API version. If an expected field is missing, confirm both before changing the payload. Record rules can impose requirements beyond describe, so retain the API error body when a write fails. ## Create and read a record Create a fictitious account: ```http POST /services/data/v{version}/sobjects/Account Content-Type: application/json {"Name":"Juniper Sample Company","Description":"Integration test fixture"} ``` Expect HTTP `201` and a body containing `id`, `success`, and `errors`. Save the returned `id` as your record ID. Read the fields your test needs: ```http GET /services/data/v{version}/sobjects/Account/{recordId}?fields=Id,Name,Description ``` Expect HTTP `200` with the selected fields. Use the returned ID for later requests. Do not construct an ID from a name or an assumed sequence. ## Update and verify a record Send only the fields to change: ```http PATCH /services/data/v{version}/sobjects/Account/{recordId} Content-Type: application/json {"Description":"Updated integration test fixture"} ``` Expect HTTP `204` with no JSON body. Read the record again and assert the stored value. Some predefined record rules derive or constrain values, so a successful write alone does not prove your intended final state. To remove the fixture, send `DELETE /services/data/v{version}/sobjects/Account/{recordId}`. A successful delete returns `204`. Use [queryAll](/apps/salesforce-query/) when your test needs to inspect soft-deleted records. ## Link records in a composite request To create an account and its contact in one request, use `/composite` with reference substitution. This example rolls back both writes if a subrequest fails: ```http POST /services/data/v{version}/composite Content-Type: application/json { "allOrNone": true, "compositeRequest": [ { "method": "POST", "url": "/services/data/v{version}/sobjects/Account", "referenceId": "account", "body": {"Name": "Juniper Sample Company"} }, { "method": "POST", "url": "/services/data/v{version}/sobjects/Contact", "referenceId": "contact", "body": {"LastName": "Example", "AccountId": "@{account.id}"} } ] } ``` Replace the version placeholder in the outer path and both subrequest URLs. Inspect each `compositeResponse` item's `httpStatusCode` and `body`. An HTTP `200` outer response can contain failed subrequests. Choose another composite operation when its transaction behavior fits your task: - Use sObject Collections for groups of record creates, reads, updates, upserts, or deletes. Inspect every record result. Write operations support `allOrNone`. - Use `/composite/batch` for independent requests. Each request commits separately. `haltOnError` stops later requests after a failure and leaves earlier successes committed. - Use `/composite/graph` for groups of dependent writes. Each graph succeeds or rolls back independently. - Use `/composite/tree/{object}` for a nested tree of new related records. ## Use external IDs without assuming a custom field Inspect describe for a field marked `externalId` or `idLookup`. Use that field only if it exists in this app version and is available to your actor. Retrieve with `GET /sobjects/{object}/{field}/{value}` or upsert with `PATCH /sobjects/{object}/{field}/{value}`, relative to the selected REST version. URL-encode the external value as one path segment. An upsert creates a record when no match exists and updates the match when one exists. Multiple matches return HTTP `300` with the matching record URLs and change nothing. Do not add a made-up `__c` field to a request to create schema. Record endpoints accept fields from the catalog. Public environment settings do not install a custom schema. See [known differences](/apps/known-differences/). --- # Query records with SOQL Run SOQL queries, follow result pages, and inspect deleted records under the chosen actor. Use SOQL to inspect test data and verify integration outcomes. Connect with the actor your integration uses, then [discover its objects and fields](/apps/salesforce-records/). ## Run a query Send a `GET` request to `/services/data/v{version}/query` with the SOQL statement in the `q` parameter. Replace `{version}` with a served API version. URL-encode the statement through your client's query-parameter API. For example, query the account from the record guide: ```sql SELECT Id, Name, Description FROM Account WHERE Name = 'Juniper Sample Company' ORDER BY Id ``` Inspect `totalSize`, `done`, and `records` in the JSON response. A successful query with no matches returns an empty result. That result may also reflect the actor's record visibility. Use `ORDER BY` when a test depends on row order. Add a unique tie-breaker such as `Id` when your primary sort field can have duplicate values. A query without an explicit order is unsuitable for an assertion about which row comes first. ## Follow every result page After processing `records`, check `done`. If it is false, request the returned `nextRecordsUrl` through the same authenticated client and process that page. Repeat until `done` is true. Keep the actor and app connection unchanged while following a locator. Do not rebuild the query with increasing `OFFSET`, append the original `q` to locator requests, or infer completion from a short page. The replica binds each locator to the user who ran the query. Locators expire two days after the query opens. An invalid, expired, or other user's locator returns `INVALID_QUERY_LOCATOR`. Restart the query when your application can safely do so. A page holds up to 2,000 records. To change that, send `Sforce-Query-Options: batchSize=N` with N from 200 to 2,000. For assertions over changing data, complete one query's pages before making further writes. The locator does not add records inserted after it opens. Ordinary query pages omit records deleted between pages, while `totalSize` stays unchanged. A locator is not an immutable copy of every record value. ## Query relationships Use describe's relationship names. A lookup field name and the corresponding relationship name are different request values. To read a contact's parent account, use a parent path: ```sql SELECT Id, LastName, Account.Name FROM Contact WHERE LastName = 'Example' ORDER BY Id ``` To read contacts under an account, use the child relationship name: ```sql SELECT Id, Name, (SELECT Id, LastName FROM Contacts ORDER BY Id) FROM Account WHERE Name = 'Juniper Sample Company' ORDER BY Id ``` Inspect each parent's nested result rather than treating child records as top-level rows. REST and SOAP accept up to four nested child levels from API version 58.0 and one level below it. Bulk query jobs accept none. ## Aggregate records For a count, use `SELECT COUNT() FROM Account`. Read the count from `totalSize`. `COUNT()` does not produce a normal record list. For grouped results, select the group field and an aggregate: ```sql SELECT Industry, COUNT(Id) FROM Account GROUP BY Industry ``` Check that describe marks the grouping field as groupable. Select only fields that are grouped or aggregated. A grouped or aggregate result larger than one page fails with `EXCEEDED_ID_LIMIT`. Add `LIMIT` to keep it on one page. ## Inspect deleted records Send the statement to `/services/data/v{version}/queryAll` to include deleted and archived records. For a deleted test account, select `Id`, `Name`, and `IsDeleted` with a filter on its returned ID. Follow `nextRecordsUrl` as returned even when it uses `/query/`. The locator retains the original queryAll behavior. Ordinary `/query` omits deleted records. ## Diagnose query failures Inspect `errorCode` and `message` before retrying. Confirm the object, field, and relationship names against describe for the same actor and API version. The replica supports selected SOQL filters, parent paths, child queries, grouping, aggregates, date literals, and semi-joins. It returns `MALFORMED_QUERY` with the message ` is not supported` for `FOR UPDATE`, `UPDATE TRACKING`, `UPDATE VIEWSTAT`, `WITH SYSTEM_MODE`, `WITH DATA CATEGORY`, other `WITH` filters, `DISTANCE()`, `FORMULA()`, and a `FROM` list with relationship aliases. `WITH SECURITY_ENFORCED` also fails. `WITH USER_MODE`, `FOR VIEW`, and `FOR REFERENCE` are accepted and change nothing. Do not use the `explain` parameter to validate Salesforce query plans. The replica does not implement that behavior. See the [known differences](/apps/known-differences/) for other query and permission gaps. --- # Import and query data with Bulk API 2.0 Upload CSV fixtures, inspect row results, and retrieve paginated query exports. Use Bulk API 2.0 when your client imports or exports CSV. Connect to the environment with an actor that can access the relevant objects and fields. The paths below use `{version}` for a supported Bulk API version and `{jobId}` for the ID returned at job creation. Send requests through your authenticated client at the app URL. Keep the same actor and API version throughout each job. ## Create an ingest job Discover the target object's fields before preparing the CSV. For a fictitious account fixture, create an insert job: ```http POST /services/data/v{version}/jobs/ingest Content-Type: application/json {"object":"Account","operation":"insert","contentType":"CSV","lineEnding":"LF","columnDelimiter":"COMMA"} ``` Save the returned job ID. The new ingest job is `Open`. Upload CSV to the job's batches resource: ```http PUT /services/data/v{version}/jobs/ingest/{jobId}/batches Content-Type: text/csv Name,Description Juniper Sample Company,First test account Willow Sample Company,Second test account ``` Finish the upload by changing the state: ```http PATCH /services/data/v{version}/jobs/ingest/{jobId} Content-Type: application/json {"state":"UploadComplete"} ``` Read `GET /services/data/v{version}/jobs/ingest/{jobId}` until the job reaches a terminal state, such as `JobComplete`, `Failed`, or `Aborted`. Keep completion polling in your integration even if a small replica job finishes before your first status request. ## Check each row's result Read the job's `numberRecordsProcessed` and `numberRecordsFailed`. Fetch the result resources under the ingest job: - `/successfulResults` contains successful rows and their Salesforce IDs. - `/failedResults` contains rejected rows and their errors. - `/unprocessedrecords` contains rows that were not processed. A completed job can contain row failures. Assert the row outcomes your test expects, then query or retrieve the stored records. A successful CSV upload only confirms receipt of the file. For updates or deletes, include record IDs in the CSV. For upserts, set `operation` to `upsert` and provide `externalIdFieldName` when creating the job. Use a field that describe marks `externalId` or `idLookup`. Any other field fails job creation with `INVALIDJOB`. The replica also implements `hardDelete`. Use it only when permanent removal is part of your test. ## Export a query as CSV Create a query job with an explicit field list: ```http POST /services/data/v{version}/jobs/query Content-Type: application/json {"operation":"query","query":"SELECT Id, Name FROM Account ORDER BY Id","contentType":"CSV"} ``` Use `queryAll` as the operation when the export must include deleted records. Save the returned job ID and read its status at `/jobs/query/{jobId}` under the same version. After `JobComplete`, request `/jobs/query/{jobId}/results`. Parse the CSV by its header names. If the `Sforce-Locator` response header contains a continuation value rather than `null`, request the same results endpoint with that value in the `locator` query parameter. Repeat until the header is `null`. Set `maxRecords` on every results request. The replica's default page is 50,000 rows, and State Machines refuses an app response larger than 4 MiB with `response_too_large`. Keep the job's API version when retrieving pages. Below API version 50.0 the CSV columns are in alphabetical order. From 50.0 they follow the `SELECT` order. A results request at a version on the other side of that boundary returns `CONFLICT`. From API version 58.0, `GET /jobs/query/{jobId}/resultPages` lists the result pages. ## Adjust a query for Bulk Bulk query jobs reject grouping, aggregate functions, `OFFSET`, `TYPEOF`, and parent-to-child subqueries. They also reject compound address and location fields. Select supported scalar component fields instead. Prefer explicit field names. `FIELDS(ALL)` and `FIELDS(CUSTOM)` are unsupported. `FIELDS(STANDARD)` expands the actor-visible standard fields, but the expanded selection still fails if it includes a compound field. Use [REST queries](/apps/salesforce-query/) when your test needs a supported query form that Bulk does not accept. Keep Bulk-specific failure assertions for clients that must report an invalid job correctly. --- # Salesforce known differences Compatibility boundaries and recorded gaps that affect assertions against the Salesforce replica. A passing test establishes the asserted behavior for one replica app version, Salesforce API version, and actor. Coverage of an endpoint does not establish every Salesforce behavior associated with that endpoint. Conformance tests compare selected requests with captured Salesforce responses. Other behaviors have local regression tests without a captured Salesforce comparison. ## Supported scope and boundaries | Area | Replica behavior | Consequence for a test | | --- | --- | --- | | Organization data | Each app version starts with a predefined catalog and data. Environment creation does not import an external organization. | Customer-specific fields and packages are available only if the app's catalog includes them. A snapshot copies a State Machines environment, not an external Salesforce organization. | | Custom schema | The internal catalog represents custom objects and fields. Public app settings do not expose schema import or deployment, and the registered routes do not provide a general Metadata API. | Internal custom-field tests do not establish a customer-facing metadata deployment workflow. | | Formulas and automation | Describe can expose calculated-field metadata. The runtime implements specific defaults, derivations, validations, and related-record effects. It has no general customer formula, Flow, or Apex execution engine. | A calculated flag or default formula in describe is not a guarantee that arbitrary automation executes. | | REST discovery | The resource directory lists a subset of registered routes. | Use the [API reference](/apps/salesforce/) for supported operation families and describe for object and field availability. | | SOAP | Partner operations are supported. `merge` and `convertLead` return `INVALID_OPERATION`. Enterprise SOAP is unsupported. | Tests that require merge or lead conversion need separate validation against Salesforce. | | Tooling | Only `WorkflowOutboundMessage` create, read, and delete routes are registered. | Tooling availability does not imply support for Apex tooling, custom-object deployment, or general metadata management. | | Outbound messages | An active outbound message sends a SOAP notification on each create or update of its configured object. | This behavior does not imply evaluation of a customer's workflow criteria. | | Change events | Selected record changes create internal events for the replica's background processing. No public route serves those events. | These internal events are not a Streaming API or Pub/Sub subscription interface. | | Bulk | Bulk API 2.0 ingest and query are implemented. Bulk API 1.0 is unsupported. Bulk query restricts query forms and compound fields. | Use the [Bulk guide](/apps/salesforce-bulk/) to select a supported query and inspect per-row failures. | | OAuth | Token grants depend on the connected app configuration and supplied credentials. No authorization endpoint is registered for a browser authorization-code flow. | Availability of `/token` does not make the replica a complete Salesforce sign-in service. | | Versions | Earlier served versions project from the release's baseline catalog. Object availability and protocol policies vary by version. | A served version is not a separate historical Salesforce installation. Keep app version and API version in test reports. | | Quotas and timing | The replica counts daily API requests but does not enforce Salesforce API limits. Bulk jobs run on the replica's own workers. | `/limits`, completion times, and throughput do not predict a customer's Salesforce capacity. | | Daily API limit | The replica never returns `REQUEST_LIMIT_EXCEEDED`. | Do not test limit-exhaustion handling against the replica. | | Bulk upload size | One Bulk API 2.0 upload is capped at 20 MiB. Salesforce accepts 150 MB. | Split large fixtures into several jobs. | | Child rows larger than a page | The replica returns the whole child set inside its parent. Salesforce pages it on its own locator. | Do not test child-locator handling against the replica. | ## Recorded compatibility gaps Each gap covers only the captured request and caller. The rest of the operation can still match Salesforce. | Scenario | Recorded difference | Test guidance | | --- | --- | --- | | SOQL query plans | The `explain` parameter is ignored. | Do not assert Salesforce optimizer plans or costs against the replica. | | Child query with `LIMIT` and no `ORDER BY` | The captured organization and replica returned different child rows. The capture does not establish a guaranteed order. | Specify an order when the chosen child row matters. | | A Standard User queries its sessions | The replica hides `LoginHistory` and `AuthSession` where the captured organization exposes the user's own session information. | Do not infer session-query parity from administrator tests. | | A Standard User requests limits | The replica returns storage limits to a Standard User. The captured organization did not. | Check the exact actor's response instead of asserting the administrator's limit keys. | | `EntityDefinition` sharing metadata | Captured administrator and sales-manager responses remain marked as divergent. | Treat sharing-metadata introspection as an unqualified scenario until the specific fields your client needs are verified. | | `AccountPartner` lifecycle | The replica does not reproduce the captured reverse partner record and `ReversePartnerId` link. | A successful partner create does not establish the reverse relationship behavior. | | An `OpportunityLineItem` query error | The code, column, and value match, but the displayed query excerpt differs. | Prefer error codes and relevant fields over exact full-message equality unless the text itself is under test. | | `USING SCOPE team` on `Product2` | The captured organization returned an internal server error. The replica answers the query and explicitly does not reproduce that failure. | Do not use the replica to reproduce this captured platform defect. | ## Other SOQL restrictions The replica returns `MALFORMED_QUERY` with the message ` is not supported` for `FOR UPDATE`, `UPDATE TRACKING`, `UPDATE VIEWSTAT`, `WITH SYSTEM_MODE`, `WITH DATA CATEGORY`, other `WITH` filters, `DISTANCE()`, `FORMULA()`, and a `FROM` list with relationship aliases. `WITH SECURITY_ENFORCED` also fails. `WITH USER_MODE`, `FOR VIEW`, and `FOR REFERENCE` are accepted and change nothing. Bulk queries impose additional restrictions on grouping, aggregates, child queries, `TYPEOF`, `OFFSET`, field expansion, and compound fields. REST and SOAP accept up to four nested child levels from API version 58.0 and one level below it. Bulk query jobs accept none. ## Difference reports A useful report includes the app version, Salesforce API version, actor, request method and path, expected result, actual result, and a small sanitized reproduction. Credentials and customer records are unnecessary. [Request troubleshooting](/troubleshooting/requests/) distinguishes a State Machines gateway failure from a Salesforce protocol response. Unsupported behavior needs separate validation against an appropriate Salesforce environment. A disposable test must not automatically fall back to production credentials. --- # Upgrade from SDK 0.5 to 0.6 Install 0.6, rename the changed calls and error codes, and confirm one complete task. SDK 0.6 follows the current management API. SDK 0.5 and older call routes that the API no longer serves, so they fail against it. ## Install 0.6 The SDK is ESM only and needs Node.js 20.20 or later. ```sh npm install @usestatemachines/sdk@0.6 ``` ## Update the key and host Rename the `STATE_API_KEY` variable to `STATEMACHINES_API_KEY`. The SDK reads only the new name. The default host is now `https://api.usestatemachines.com`. If your code passes `baseUrl: 'https://state-api.usestatemachines.workers.dev'`, delete that option. ## Rename lifecycle calls | 0.5 | 0.6 | | --- | --- | | `sm.environments.start(input)` | `sm.environments.create(input)`, which waits until `running` | | `sm.environments.start({ snapshotId })` | `sm.environments.create({ snapshotId })` | | `env.stop()`, `sm.environments.stop(id)` | `env.delete()`, `sm.environments.delete(id)` | | `env.suspend()`, `sm.environments.suspend(id)` | `env.pause()`, `sm.environments.pause(id)` | | `env.resume()` then `env.waitUntilReady()` | `env.resume()`, which waits until `running` | | `waitUntilReady`, `waitUntilStopped`, `waitUntilSuspended` | `sm.environments.wait(id, { status })` | Pausing keeps the environment's data. Deleting removes it. Map each old `stop` call to `delete` only where the task is finished with the data. ## Update app entries and credentials | 0.5 | 0.6 | | --- | --- | | Apps as `{ key, appId }` | Apps as `{ app, name }`, or the app name as a string | | `env.connect(app, { apiId, actorId })` returns a `Connection` | `env.connect(app, { actorId })` returns `Credentials`. `fetch` picks the API from the path. | | `connection.baseUrl`, `connection.headers()` | `credentials.url`, `credentials.headers`, `credentials.token` | | `X-State-Environment-Token` header | `StateMachines-Environment-Token` header | For Salesforce SOAP, put `credentials.token` in `SessionHeader/sessionId` in the envelope. See [credentials and actors](/environments/credentials/). ## Update snapshots, requests and identity | 0.5 | 0.6 | | --- | --- | | `env.snapshot(options)` | `env.snapshots.create(options)` | | `sm.snapshots.capture(environmentId, options)` | `sm.snapshots.create({ environmentId, ...options })` | | `sm.environments.listRequests` | `sm.requests.list({ environmentId })` | | `sm.environments.getRequest` | `sm.requests.get(id)` | | `sm.environments.getRequestPayload` | `sm.requests.body(id, direction)` | | `sm.environments.getRequestStats` | `sm.requests.stats(query)` | | `sm.credential.get()` | `sm.me()` | | The catalog | `sm.apps.list()`, `sm.apps.versions(appId)` | ## Update error handling | 0.5 | 0.6 | | --- | --- | | `TimeoutError` | `WaitTimeoutError` | | `error.retryAfter` | `error.retryAfterMs` | | `environment-limit` | `environment_limit_reached` | | `capture-in-progress` | `snapshot_in_progress` | Every 0.6 error code uses underscores. Check the error subclasses with `instanceof` before you read `StateMachinesError.code`. Never match message text. The [SDK error reference](/sdk-api/errors/) lists the classes and codes. ## Confirm one complete task Run the [quickstart](/get-started/quickstart/) outside production. Confirm that creation reaches `running`, the app request succeeds, and deletion completes. ## Pin the minor version A `0.x` minor release can include breaking changes. Pin the minor version in `package.json` so an install does not move you to 0.7: ```json { "dependencies": { "@usestatemachines/sdk": "~0.6.0" } } ``` --- # Keep test environments isolated Own creation and deletion in one awaited helper and assert native app behavior. Give each independent test run an environment it owns. Keep the create call and deletion together so an assertion failure cannot skip cleanup. Share a prepared snapshot when tests need the same starting records, rather than sharing one mutable environment. ## Wrap the test body This helper accepts a test function, gives it Salesforce credentials, and deletes the environment after the function settles. The final call checks a native response status. ```ts import { type Credentials, StateMachines } from '@usestatemachines/sdk'; const sm = new StateMachines(); export async function withSalesforce( run: (salesforce: Credentials) => Promise, ): Promise { const env = await sm.environments.create( { apps: ['salesforce'] }, { wait: false }, ); try { await sm.environments.wait(env.id, { status: 'running' }); return await run(await env.connect('salesforce')); } finally { await env.delete(); } } await withSalesforce(async (salesforce) => { const response = await salesforce.fetch('/services/data/v67.0/limits'); if (!response.ok) { throw new Error(`Salesforce returned ${response.status}`); } }); ``` The `return await` inside the helper matters. It waits for the test body before entering `finally`. Returning an unawaited promise would let deletion race with the app requests. Adapt the callback to your test runner's assertions and give the test enough time for environment startup and cleanup. ## Assert the behavior your integration needs A successful create call verifies that the environment started. It does not verify the integration. Exercise your actual integration path, then assert the native response or query the resulting records. Use an actor with the permissions the scenario expects. An administrator-only test cannot establish that the same operation works for a restricted actor. Read actors from the version that the environment runs. Check `response.ok` for native requests. A Salesforce error response is not automatically thrown by `credentials.fetch()`. ## Reuse data without sharing writes Prepare a snapshot once, wait until it is `ready`, and create a separate environment from its ID for each independent run. The snapshot stays unchanged as tests modify their copies. See [snapshot reuse](/snapshots/reuse/). Choose concurrency using the applicable `concurrentEnvironments` limit from `sm.me()`. Starting and running environments count toward that limit. A failed test must still reach its awaited deletion. After a network interruption during a native write, do not wrap the write in a generic retry loop. Determine whether the record was created before repeating it. Management create idempotency does not extend to Salesforce writes. ## Own cleanup across CI retries Use the CI secret store for `STATEMACHINES_API_KEY` and give the key the permissions needed for create, connect, reads, and deletion. Use one environment per independent test run. Add a run identifier to `labels` so a recovery job can find the environments created by that run. Keep the accepted environment ID and create idempotency key with the CI job when interrupted jobs need recovery. An ordinary test failure reaches `finally`. A forced process termination may not. A later cleanup job can list environments with that run label and delete only the IDs the job owns. The example's work must run inside a test runner timeout that leaves time for deletion. Passing an aborted test signal to deletion prevents cleanup. Report a deletion error along with the environment ID so cleanup can be retried. A job retry must decide whether it is resuming the same operation or starting another independent test. Reuse the original create key only for the former. A new test with a new key gets separate data. Use the [recovery guide](/sdk-api/recovery/) for an uncertain create response. --- # Recover interrupted management operations Use idempotency keys, explicit waits, and resource IDs when a call has an uncertain outcome. A timeout does not prove that creation failed. The server may have accepted the request before the response was lost. Replaying the same create operation with the same idempotency key avoids creating a second environment. ## Preserve the operation identity Choose an `idempotencyKey` before calling `environments.create()`. Save it with the exact create input if the caller must recover after a process restart. The SDK already reuses one key across its automatic retries. Replay uncertain creation with the same key, input, workspace, and caller identity. An API key is part of that identity. Replacing it with a different key does not preserve the same create operation. A different input with that key fails with `idempotency_key_reused`. Use a new key only for a new operation. ```ts import { type CreateEnvironment, StateMachines, StateMachinesError, } from '@usestatemachines/sdk'; const sm = new StateMachines(); const input: CreateEnvironment = { apps: ['salesforce'], name: 'Recoverable run', }; const idempotencyKey = crypto.randomUUID(); console.log('Save this operation key for recovery:', idempotencyKey); async function createAccepted() { try { return await sm.environments.create(input, { idempotencyKey, wait: false }); } catch (error) { if (!(error instanceof StateMachinesError)) { throw error; } const unknownOutcome = error.status === null && error.code === null; const transientStatus = error.status === 502 || error.status === 503 || error.status === 504; if (!unknownOutcome && !transientStatus) { throw error; } return sm.environments.create(input, { idempotencyKey, wait: false }); } } const env = await createAccepted(); try { const running = await sm.environments.wait(env.id, { status: 'running' }); console.log(running.id, running.status); } finally { await env.delete(); } ``` For restart recovery, persist the key and input outside the process before the first call. Printing the key as this demonstration does makes it visible, but is not a durable job record. This example makes one explicit replay after the SDK exhausts its own retries. The SDK retries for up to 10 minutes first. If both calls fail without a known outcome, keep the printed operation key and the input. Recover that operation later. Do not replace the key just to get past the error. A replay returns the same resource as it is now. If it was deleted, replay does not create a replacement. With the default wait, a replay of a deleted environment throws `UnexpectedStatusError`, or `EnvironmentFailedError` if it was deleted because it failed. Use `wait: false` to inspect a recovered resource before deciding the next action. Snapshot creation accepts the same idempotency option. The key identifies the create operation, not the future native writes you make inside an environment. ## Separate acceptance from waiting Use `{ wait: false }` when your code needs the environment ID before startup finishes. Put the subsequent wait inside the same `try` whose `finally` deletes the environment. `WaitTimeoutError` means the SDK stopped waiting. The resource may still become ready. Read it again, wait again, or delete it when the task no longer needs it. Aborting a wait also does not undo an accepted operation. ## Classify the failure Check errors with `instanceof`. `EnvironmentFailedError` carries the environment and its failure. `SnapshotFailedError` carries the failed snapshot. `UnexpectedStatusError` means the resource cannot reach the requested status without another action. `StateMachinesError` provides `code`, `status`, `requestId`, `fields`, and `retryAfterMs`. Inspect the code and preserve the operation identity before deciding to replay. The [SDK error reference](/sdk-api/errors/) describes each class and code. The SDK retries transient management failures. Native app requests made with `credentials.fetch()` are never retried. After an uncertain Salesforce write, inspect the app state before deciding whether another write is safe. ## Bound waits without losing ownership A wait timeout carries `resource`, the last successful resource read, which can be null. Keep the accepted ID separately even if no poll succeeds. If you pass a `signal`, check its `aborted` state before you classify the error. Keep cleanup outside the aborted signal's scope. For snapshot creation, also retain the accepted snapshot ID before waiting. A `saving` snapshot cannot be deleted. Either leave its source running and resume observation, or end the source and wait for the interrupted save to settle before deleting snapshot metadata. A cleanup failure is a separate failure from the task. Preserve enough job state to retry deletion, and report the cleanup error without discarding the original task failure. A `finally` block cannot run after a process crash or a forced CI termination. --- # Read every page of a list Traverse management API lists with opaque cursors and consistent filters. Every SDK `list()` call returns one page with `data` and `nextCursor`. A successful call does not necessarily return every matching resource. Use pagination for environment cleanup, fixture selection, app discovery, and request inspection whenever the first page may be incomplete. ## Continue until the cursor is null Process each page's `data`, then pass its `nextCursor` as the next call's `cursor`. Stop when `nextCursor` is `null`. ```ts import { StateMachines } from '@usestatemachines/sdk'; const sm = new StateMachines(); let cursor: string | undefined; do { const page = await sm.environments.list({ cursor }); for (const env of page.data) { console.log(env.id, env.name, env.status); } cursor = page.nextCursor ?? undefined; } while (cursor !== undefined); ``` The example lists environments without changing them. The same loop applies to snapshots, apps, versions, workspaces, requests, and audit events, using the relevant method and required filters. [List filters](/sdk-api/client/#list-filters) lists the filters each method accepts. Treat cursors as opaque strings. Do not decode, edit, or construct them. Use the cursor returned by the immediately preceding page of the same query. ## Keep filters consistent Carry the same filters into every call. For example, a request list must retain `environmentId`, and a filtered environment list must retain its status selection. The API binds cursors to the query scope. Changing filters while reusing a cursor can fail with `invalid_request` and a `fields.cursor` message. Start from the first page after changing filters. Environment and snapshot lists leave deleted resources out unless the status filter asks for them. If a known environment is absent from the default list, read it by ID or explicitly include `deleted` before assuming it never existed. ## Choose a page size `limit` defaults to 50 and accepts 1 to 100. Process pages incrementally for larger result sets. If your next action is destructive, select the intended resource IDs before deleting anything. A broad list loop is not a safe substitute for identifying which environments belong to the task. For versions and actors, match the environment's actual `versionId` after reading the relevant pages. Selecting the first version in the first page can choose a different version than the environment runs. See [credentials and actors](/environments/credentials/). --- # Use the SDK in Cloudflare Workers Pass credentials explicitly and give environment operations an owner that can await cleanup. The State Machines SDK runs in Cloudflare Workers. A Worker receives secrets as bindings on `env`, not through `process.env`. Pass your API key to the client explicitly. Store the key as a secret binding and construct the client with `new StateMachines({ apiKey: env.STATEMACHINES_API_KEY })`. Never include the key in a browser bundle or return it in an HTTP response. ## Give the operation enough lifetime Environment startup can outlast a short HTTP request. Choose a job owner that can await creation, startup, the app operation, and deletion, such as a Queue consumer, a Workflow step or a Durable Object. The function below is for that owner to await. It is not an HTTP handler or a fire-and-forget task. ```ts import { StateMachines } from '@usestatemachines/sdk'; export async function runSalesforceJob(apiKey: string): Promise { const sm = new StateMachines({ apiKey }); const env = await sm.environments.create( { apps: ['salesforce'] }, { wait: false }, ); try { await sm.environments.wait(env.id, { status: 'running' }); const salesforce = await env.connect('salesforce'); const response = await salesforce.fetch('/services/data/v67.0/limits'); if (!response.ok) { throw new Error(`Salesforce returned ${response.status}`); } await response.arrayBuffer(); return response.status; } finally { await env.delete(); } } ``` The example accepts creation with `wait: false`, obtains the environment ID, and waits for readiness inside `try`. Its `finally` awaits deletion even when startup or the app request fails. The example reads the response body so the Worker does not leave it unread. Calling this function without awaiting it does not provide reliable cleanup. The surrounding execution must remain alive until the function finishes. ## Split a job across requests when needed For a request-driven workflow, create with `wait: false`, persist the returned environment ID with the job, and read its status in later requests. Only call the app when the status is `running`. The job must retain ownership of deletion on success, failure, and cancellation. Persist the idempotency key before creation if a retry must recover an uncertain response. See [operation recovery](/sdk-api/recovery/). Do not return raw credentials so a browser can poll an app. Management polling belongs on the trusted server using the persisted environment ID. ## Set independent deadlines `attemptTimeoutMs` bounds each management HTTP attempt. `timeoutMs` bounds an SDK wait. A caller's `signal` can cancel the wait, but it does not delete the environment. Choose deadlines that fit the surrounding execution and leave time for cleanup. If the execution cannot guarantee that lifetime, use persisted job ownership rather than increasing a request timeout alone. --- # Define an environment in YAML Parse a readable environment definition with the same create fields and validation errors. Use YAML when an environment's app selection and task metadata belong in configuration. `parseEnvironmentYaml()` turns a YAML document into input for `sm.environments.create()`. ## Write a create definition The document uses the fields of the create input. The example defines a name, one app, and a string label, then passes the parsed input to the SDK. ```ts import { parseEnvironmentYaml, StateMachines } from '@usestatemachines/sdk'; const input = parseEnvironmentYaml(` name: CRM integration apps: - salesforce labels: suite: integration `); const sm = new StateMachines(); const env = await sm.environments.create(input, { wait: false }); try { const running = await sm.environments.wait(env.id, { status: 'running' }); console.log(running.id, running.status); } finally { await env.delete(); } ``` For a file-based configuration, read the file as UTF-8 and pass its text to `parseEnvironmentYaml()`. File access belongs to your application. The parser does not load files or process environment variables for you. Use named app objects when the same app appears more than once. For snapshot-based creation, provide `snapshotId` instead of an app list. See [Create the apps your task needs](/environments/create/) and [snapshot reuse](/snapshots/reuse/) for the respective inputs. ## Handle validation before creating resources Invalid YAML or invalid create input throws `StateMachinesError` with code `invalid_request` and a null HTTP status. The `fields` property maps dotted field paths to messages. The parser requires a top-level mapping, string keys, and exactly one of `apps` and `snapshotId`. Top-level fields are `name`, `labels`, `apps`, `snapshotId`, `workspaceId`, `timeLimitMinutes` and `tokenInUrl`. An app entry is a string or a mapping with `app`, `name` and `settings`. The parser rejects any other field. Labels must be strings. Quote values such as `"true"` or `"123"` when they are labels. The parser refuses anchors, aliases, tags, and duplicate keys. Keep the definition explicit so the effective app settings do not depend on YAML expansion rules. ## Keep secrets outside the definition An environment definition describes apps, settings, and metadata. The management API key belongs in the process or runtime secret store, not the YAML file. Avoid storing tokens or customer data in names and labels. Those fields are resource metadata, not secret storage. App settings are passed to the app unchanged and still need to satisfy that app's accepted configuration. Parsing checks field shapes. The API still validates app IDs, unique app names, size constraints, numeric bounds, workspace access, and snapshot availability. Parsing a document does not start an environment or validate every app-specific setting. A refused app setting can still fail startup with `invalid_settings`. Keep waiting and cleanup inside the same `try` and `finally`, as the example does. --- # TypeScript SDK reference Constructor options, workspace scope, public operations, and environment helpers. `StateMachines` is the client class of `@usestatemachines/sdk`. This page describes version 0.6. To move from 0.5, see [Upgrade from SDK 0.5 to 0.6](/sdk-api/upgrading/). ## Constructor options | Option | Behavior | | --- | --- | | `apiKey` | API key or member access token. Defaults to `STATEMACHINES_API_KEY` where a process environment exists. | | `baseUrl` | Management API origin. Defaults to `https://api.usestatemachines.com`. The SDK adds `/v1` resource paths. | | `workspaceId` | Default workspace for scoped calls that do not supply one. | | `fetch` | Custom fetch implementation, used for both management calls and `credentials.fetch()`. Defaults to the runtime's fetch. | | `attemptTimeoutMs` | Positive, finite timeout for each management HTTP attempt. Each retry gets a fresh timeout. Defaults to 60000 (60 seconds). It does not configure native app requests. | The SDK does not load `.env` files. A missing key, or a key that contains anything other than printable ASCII, including a space, throws `TypeError` in the constructor. Invalid `attemptTimeoutMs` throws `RangeError`. ## Workspace scope An API key belongs to one workspace. A `workspaceId` option does not grant it access to another workspace. Member access tokens need a workspace for workspace-scoped calls, supplied in the constructor or the individual input. The constructor default applies to `environments.create()`, `environments.list()`, and `snapshots.list()`. It also applies to `auditEvents.list()` when there is no `environmentId` filter. An explicit input `workspaceId` takes precedence. Calls that identify a resource by ID use that resource's workspace and still check authorization. `sm.me()` returns the principal, the `user` when the caller is a member, the current `organization`, the `organizations` the caller belongs to, permissions, workspace, and limits. `workspaceId` and `limits` can be null. Limits include `concurrentEnvironments` and the default and maximum `timeLimitMinutes`. `sm.workspaces.list({ archived, limit, cursor })` reads workspace metadata. The `archived` filter is the string `'true'` or `'false'`, with `'false'` as the default. An API key sees its own workspace. `sm.workspaces.get(workspaceId)` reads one workspace. The public SDK does not create or archive workspaces. ## Public operations | Method | Input and result | | --- | --- | | `sm.me()` | Returns the caller's identity, scope, permissions, and limits. | | `sm.apps.list(query?)` | Returns one page of apps. Query accepts `limit` and `cursor`. | | `sm.apps.versions(appId, query?)` | Returns one page of published and retired versions, including APIs, actors, and `isDefault`. | | `sm.workspaces.list(query?)` | Returns one page of visible workspaces. | | `sm.workspaces.get(workspaceId)` | Returns workspace metadata. | | `sm.environments.create(input, options?)` | Creates from `apps` or `snapshotId`. Waits for `running` by default. | | `sm.environments.list(query?)` | Returns one page of environments with helpers. | | `sm.environments.get(environmentId)` | Returns an environment with helpers, including deleted metadata. | | `sm.environments.update(environmentId, body)` | Changes `name`, `labels`, or both. `name: null` clears the name. `labels` replaces the whole map. | | `sm.environments.pause(environmentId, options?)` | Returns the paused environment when the API accepts the change. | | `sm.environments.resume(environmentId, options?)` | Resumes a paused or failed environment. Waits for `running` by default. | | `sm.environments.delete(environmentId, options?)` | Returns the deleted environment. Safe to repeat. | | `sm.environments.wait(environmentId, options)` | Reads until `options.status` is reached or the wait fails. | | `sm.environments.connect(environmentId, appName, options?)` | Returns [app credentials](/environments/credentials/), optionally for `actorId`. | | `sm.snapshots.create(input, options?)` | Accepts `{ environmentId, name? }`. Waits for `ready` by default. | | `sm.snapshots.list(query?)` | Returns one page of snapshots. | | `sm.snapshots.get(snapshotId)` | Returns snapshot metadata and save status. | | `sm.snapshots.update(snapshotId, { name })` | Renames a snapshot. `name: null` clears its name. | | `sm.snapshots.delete(snapshotId)` | Deletes a ready or failed snapshot. Safe to repeat. A saving snapshot cannot be deleted. | | `sm.snapshots.wait(snapshotId, options)` | Reads until `options.status` is reached or the wait fails. | | `sm.requests.list(query)` | Returns one page of recorded calls for the required `environmentId`. | | `sm.requests.get(requestId)` | Returns a recorded call with headers and body previews. | | `sm.requests.stats(query)` | Returns counts and latency for an environment and time window. | | `sm.requests.body(requestId, direction)` | Returns recorded bytes as `Uint8Array`. `direction` is `'request'` or `'response'`. | | `sm.auditEvents.list(query?)` | Returns one page of management audit events for a workspace or environment. | A snapshot is plain data and has no bound methods. [Requests and audit events](/environments/observability/) covers recorded bodies, time windows, and audit events. ## List filters Every `list()` method and `apps.versions()` accept `limit` and `cursor`. [Read every page of a list](/sdk-api/pagination/) shows the cursor loop. The additional filters are: | Method | Filters | | --- | --- | | `apps.list()` and `apps.versions(appId)` | No additional query filters. Versions include retired entries. | | `workspaces.list()` | `archived: 'true'` or `'false'`, selecting archived or active workspaces. | | `environments.list()` | `workspaceId`, `status` array, `snapshotId`, `search` for part of the name, and `label` array. | | `snapshots.list()` | `workspaceId`, `environmentId`, and `status` array. | | `requests.list()` | Required `environmentId`, plus the request filters in [Requests and audit events](/environments/observability/#recorded-requests). | | `auditEvents.list()` | `workspaceId` or `environmentId`. An environment filter sets the scope. An explicit workspace that conflicts with it fails. | Environment labels use `key=value` strings. Every supplied label must match. A status array selects any listed status. Environment and snapshot lists are newest first. A page is a read at that time, not a frozen export of the full result set. ## Operation options | Operation | Options | | --- | --- | | Environment and snapshot creation | `wait`, `timeoutMs`, `signal`, `idempotencyKey` | | Environment resume | `wait`, `timeoutMs`, `signal` | | Environment pause and delete | `signal` | | Environment and snapshot wait | Required `status`. Optional `timeoutMs` and `signal` | | Connect | Optional `actorId` | `wait` defaults to true. With `wait: false`, creation and resume return after acceptance. `timeoutMs` bounds the subsequent wait, starting after the initial operation returns. It is not a deadline for the combined creation and waiting time. With `wait: false`, `timeoutMs` has no effect. A create call without `idempotencyKey` generates one UUID and reuses it across automatic retries. Separate create calls generate separate keys. [Operation recovery](/sdk-api/recovery/) explains how to preserve one key across application retries. `signal` cancels caller-side work. It does not roll back an accepted operation. Only environment create, pause, resume and delete, snapshot create, and the two `wait()` methods accept `signal`. A wait's `timeoutMs` defaults to 600000 (10 minutes). Polling starts at 1 second, multiplies the interval by 1.5 up to 5 seconds, and varies each interval at random by up to 20 percent. Each poll read times out after 30 seconds. ## Environment helpers Returned environments have `connect()`, `pause()`, `resume()`, `delete()`, `refresh()`, and `snapshots.create()` bound to their ID. The bound snapshot helper accepts `{ name? }` and the same creation options as `sm.snapshots.create()`. Helpers are not enumerable. `JSON.stringify(env)` and object spread preserve API data, not methods. A parsed JSON environment must be read again through `sm.environments.get(id)` to obtain helpers. An environment object is one resource read. Operations return new objects. They do not change a previously returned object's `status`. `refresh()` reads the current state. Explicit waits are on `sm.environments.wait()`, not on the environment object. ## Runtime and type support The SDK is ESM only. Node.js support follows the package's `engines` field. TypeScript projects need fetch types through the DOM library or Node.js types. Response timestamps are `Date` objects in the SDK. Direct HTTP responses use timestamp strings. Public types and the error classes are exported from `@usestatemachines/sdk`. Response enums can contain values introduced after the installed SDK, so callers need a fallback for unfamiliar statuses. Polling keeps waiting on an unfamiliar status until its deadline. The SDK belongs in trusted server processes. A management API key must never be included in browser JavaScript. Cloudflare Workers use an explicitly supplied key. See [Use the SDK in Cloudflare Workers](/sdk-api/workers/). --- # SDK error reference Public error classes, retry boundaries, cancellation, and management error codes. The SDK separates failed management calls from failed waits. Native app responses have their own error formats and are returned as `Response` objects by `credentials.fetch()`. ## Public error classes | Class | Meaning and available data | | --- | --- | | `StateMachinesError` | A management call failed, a response was invalid, or local validation refused input. Fields are `message`, `status`, `code`, `requestId`, `fields`, and `retryAfterMs`. | | `EnvironmentFailedError` | A wait cannot reach its target because the environment failed, including deletion with reason `failed`. Extends `StateMachinesError` and carries `environment`. Read the startup failure through `environment.failure`. | | `SnapshotFailedError` | A wait encountered a failed snapshot. Extends `StateMachinesError` and carries `snapshot`. Read `snapshot.failure`. | | `UnexpectedStatusError` | A known status cannot reach the target without another action. Carries `resource` and its actual `status`. Extends `Error`, not `StateMachinesError`. | | `WaitTimeoutError` | The whole wait deadline expired. Carries `timeoutMs` and `resource`, the last successful read or null if there was none. Extends `Error`, not `StateMachinesError`. | Check specific subclasses before `StateMachinesError`. A failure code such as `invalid_settings` belongs to `environment.failure.code`. It is not the `code` property of `EnvironmentFailedError`. For `StateMachinesError`, `status` is the HTTP status or null. A null value can mean no response arrived, local validation failed, or response parsing failed. `code` can also be null. Neither field alone establishes whether the server accepted a mutation. `fields` maps dotted input paths to arrays of messages. `requestId` identifies a management request when available. `retryAfterMs` converts the server's `Retry-After` value into milliseconds. These properties can be null. ## Polling outcomes `wait(id, { status })` checks the target before classifying failure. Waiting explicitly for `failed` resolves when that status is read. Waiting for `running` from `paused` throws `UnexpectedStatusError` because resumption needs a separate call. Environment waits understand `starting`, `running`, `paused`, `failed`, and `deleted`. Snapshot waits understand `saving`, `ready`, `failed`, and `deleted`. An unfamiliar response status is polled until the target or deadline, allowing for server states introduced after this SDK. Polling retries connection failures, read timeouts, and HTTP 502, 503, and 504 with backoff. A `Retry-After` value can delay the next poll. Other HTTP failures, such as authentication or validation failures, stop the wait. Each poll read times out after 30 seconds or at the wait deadline, whichever comes first. The polling loop owns its retries rather than nesting the generated client's retry loop. ## Timeout and cancellation `attemptTimeoutMs` is a constructor option for each management HTTP attempt. `timeoutMs` is an option for the wait as a whole. In create and resume helpers, the wait begins after the initial operation succeeds. `timeoutMs` defaults to 600000. A negative value throws `RangeError`. Zero expires without reading the resource. Aborting an explicit wait rejects with the caller's `signal.reason`. Aborting the initial management request rejects with `StateMachinesError`, whose cause records the abort. Application code can check its own signal's `aborted` state to distinguish cancellation from another failure. Cancellation and timeout stop observation. They do not delete an environment, stop snapshot saving, or reverse a resume. [Recovery](/sdk-api/recovery/) describes how to retain IDs and resume observation. ## Management error codes The SDK's `ErrorCode` type and `openapi.json` contain the full error vocabulary. Codes relevant to environment work include: | Code | Status | Meaning for the caller | | --- | --- | --- | | `invalid_request` | 400 | Input failed validation. Inspect `fields` and correct the input before resubmitting. | | `unauthenticated` | 401 | The management bearer credential is absent, invalid, expired, or revoked. | | `forbidden` | 403 | The caller lacks the required permission or organization context. | | `not_found` | 404 | The resource is absent or outside the caller's permitted scope. | | `invalid_status` | 409 | The resource cannot perform the operation in its current status. | | `snapshot_in_progress` | 409 | This environment already has a snapshot saving. | | `idempotency_key_reused` | 409 | The same create key was reused with different input. | | `workspace_archived` | 409 | The workspace does not accept this change while archived. | | `body_too_large` | 413 | The management request body exceeds the accepted size. | | `version_unavailable` | 422 | The app has no default version. | | `environment_limit_reached` | 429 | The caller has reached the applicable concurrent environment allowance. | | `internal_error` | 500 | The service failed to complete the request. Preserve the request ID for diagnosis. | | `unavailable` | 503 | A management dependency or the service is temporarily unavailable. | A code is not a general instruction to retry. The SDK retries a management call after a connection failure, an attempt timeout, or HTTP 502, 503 or 504. It backs off from 1 second to 5 seconds and stops retrying after 10 minutes. It does not retry any 4xx status, 429 or 500. It never retries native app requests. Error messages avoid embedding raw management response bodies. Application logs still need to exclude credential objects, custom fetch inputs, and bearer headers. --- # Management API reference HTTP authentication, lifecycle responses, idempotency, and the error format without the TypeScript SDK. The management API owns State Machines resources. It is separate from the native API of an app running in an environment. The default origin is `https://api.usestatemachines.com` and resource routes use `/v1`. The [management endpoint reference](/sdk-api/endpoints/) lists every public route, request field, and response shape from `openapi.json`. ## Authentication Management calls use `Authorization: Bearer` with a State Machines API key or member access token. API keys are scoped to one workspace. `GET /v1/me` returns the caller's principal, permissions, workspace, and applicable limits. Do not pass the management API key to Salesforce. Request app credentials through the management API, then send those credentials only to the returned app URL. For JSON request bodies, send `Content-Type: application/json`. Use the same field names as the SDK create input. Supply exactly one of `apps` and `snapshotId`. Member tokens must identify the target `workspaceId` on workspace-scoped calls. An API key defaults to its own workspace. ## Acceptance and readiness | Operation | Response | | --- | --- | | `POST /v1/environments` | Create an environment. Returns 202, or 200 on a replay. | | `POST /v1/snapshots` | Create a snapshot. Returns 202, or 200 on a replay. | | `POST /v1/environments/{environmentId}/pause` | Returns the environment after the API accepts the change. | | `POST /v1/environments/{environmentId}/resume` | Returns the accepted environment in `starting`. | | `DELETE /v1/environments/{environmentId}` | Returns the environment after the API accepts the change. | A replay returns the resource as it is now. A successful environment create response means the environment exists, usually in `starting`. Poll the environment until it is `running` before making app requests. A snapshot must become `ready` before use. Unlike the SDK's default behavior, direct HTTP creation does not wait for readiness. Pause and delete take effect when the API answers. The gateway refuses app calls from that point. Compute and data cleanup continue after the delete response, so HTTP 200 is not proof that every underlying cleanup step has finished. After resume, poll until the environment is `running` before requesting app credentials. ## Idempotency Environment and snapshot creation accept the `Idempotency-Key` header. Pause, resume and delete also accept the header. A key is 8 to 128 characters from `A-Z`, `a-z`, `0-9`, `.`, `_`, `:` and `-`. Replaying the same key with the same input and caller recovers the accepted operation. Changed input produces `idempotency_key_reused`. Without the header, a retried create makes a second environment. A replay does not restart a resource that has since failed or been deleted. ## Errors Management errors return this body: ```json { "error": { "code": "invalid_request", "message": "...", "requestId": "req_...", "fields": { "name": ["..."] }, "status": "..." } } ``` `code`, `message` and `requestId` are always present. `fields` maps dotted input paths to arrays of messages. `status` is the resource status involved in an invalid operation, distinct from the HTTP status number. Both are optional. The `Request-Id` response header also identifies the management request. Preserve the request ID when reporting a failure. Never include bearer tokens in diagnostic output. The [SDK error reference](/sdk-api/errors/#management-error-codes) lists the codes and their HTTP statuses. ## Lists Lists return `{ data, nextCursor }`. Repeated list filters use repeated query parameters, such as `status=running&status=paused` or `label=suite%3Dintegration`. HTTP time filters use RFC 3339 strings, while the SDK accepts `Date` objects. Continue with `cursor` until `nextCursor` is `null`. See [Read every page of a list](/sdk-api/pagination/). ## Clients in other languages Preserve one create key across transport retries and use bounded polling with backoff. Inspect terminal statuses before the next poll. Never send the management bearer token to an app URL. Do not retry native app writes automatically. [Recover interrupted management operations](/sdk-api/recovery/) describes the caller behavior. --- # Management endpoint reference Routes, parameters, request schemas, and responses generated from the public API contract. This reference derives from the same [OpenAPI contract](/openapi.json) that ships in `@usestatemachines/sdk`. It covers the management operations available to API-key callers. Member-only dashboard administration and vendor-native app APIs have separate contracts. Requests use a workspace API key in the `Authorization: Bearer` header. The [management API reference](/sdk-api/management-api/) explains authentication, workspace scope, and how accepted lifecycle requests become usable resources. The [permissions guide](/environments/access/) describes which actions a key can perform. Each heading is an OpenAPI operation ID, followed by its HTTP method and route. Request bodies below are schemas, not example payloads. The schema's `required` list identifies its declared mandatory fields. Service rules can add conditional requirements, such as choosing apps or a snapshot, or supplying both ends of a request time window. The task guides describe those rules and permission checks. References such as `#/components/schemas/Environment` point to the models in the downloadable contract. Nested references remain in their original form. Response tables show the statuses declared in the contract. The contract shares an error schema across routes. Most routes return only some of these errors. An HTTP success can mean a lifecycle operation was accepted rather than completed. Native app calls return their own responses instead of this management error format. ## listApps `GET /v1/apps` **Parameters** | Name | Location | Required | Schema | | --- | --- | --- | --- | | `limit` | query | No | `{"default":50,"maximum":100,"minimum":1,"type":"integer"}` | | `cursor` | query | No | `{"maxLength":2048,"type":"string"}` | **Responses** | Status | Meaning | Content type | Schema | | --- | --- | --- | --- | | 200 | The apps an environment can run | application/json | `{"properties":{"data":{"items":{"$ref":"#/components/schemas/App"},"type":"array"},"nextCursor":{"type":["string","null"]}},"required":["data","nextCursor"],"type":"object"}` | | 400 | Invalid request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 401 | Authentication required | application/json | `{"$ref":"#/components/schemas/Error"}` | | 403 | Permission denied | application/json | `{"$ref":"#/components/schemas/Error"}` | | 404 | Resource not found | application/json | `{"$ref":"#/components/schemas/Error"}` | | 409 | Conflicting request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 413 | Request body too large | application/json | `{"$ref":"#/components/schemas/Error"}` | | 422 | Version unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | | 429 | Limit reached | application/json | `{"$ref":"#/components/schemas/Error"}` | | 500 | Unexpected failure | application/json | `{"$ref":"#/components/schemas/Error"}` | | 503 | Dependency unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | ## listVersions `GET /v1/apps/{appId}/versions` **Parameters** | Name | Location | Required | Schema | | --- | --- | --- | --- | | `appId` | path | Yes | `{"pattern":"^[a-z][a-z0-9-]{1,39}$","type":"string"}` | | `limit` | query | No | `{"default":50,"maximum":100,"minimum":1,"type":"integer"}` | | `cursor` | query | No | `{"maxLength":2048,"type":"string"}` | **Responses** | Status | Meaning | Content type | Schema | | --- | --- | --- | --- | | 200 | The app's published versions, newest first | application/json | `{"properties":{"data":{"items":{"$ref":"#/components/schemas/Version"},"type":"array"},"nextCursor":{"type":["string","null"]}},"required":["data","nextCursor"],"type":"object"}` | | 400 | Invalid request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 401 | Authentication required | application/json | `{"$ref":"#/components/schemas/Error"}` | | 403 | Permission denied | application/json | `{"$ref":"#/components/schemas/Error"}` | | 404 | Resource not found | application/json | `{"$ref":"#/components/schemas/Error"}` | | 409 | Conflicting request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 413 | Request body too large | application/json | `{"$ref":"#/components/schemas/Error"}` | | 422 | Version unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | | 429 | Limit reached | application/json | `{"$ref":"#/components/schemas/Error"}` | | 500 | Unexpected failure | application/json | `{"$ref":"#/components/schemas/Error"}` | | 503 | Dependency unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | ## listAuditEvents `GET /v1/audit-events` **Parameters** | Name | Location | Required | Schema | | --- | --- | --- | --- | | `limit` | query | No | `{"default":50,"maximum":100,"minimum":1,"type":"integer"}` | | `cursor` | query | No | `{"maxLength":2048,"type":"string"}` | | `workspaceId` | query | No | `{"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$","type":"string"}` | | `environmentId` | query | No | `{"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$","type":"string"}` | **Responses** | Status | Meaning | Content type | Schema | | --- | --- | --- | --- | | 200 | A workspace's or an environment's audit events, newest first | application/json | `{"properties":{"data":{"items":{"$ref":"#/components/schemas/AuditEvent"},"type":"array"},"nextCursor":{"type":["string","null"]}},"required":["data","nextCursor"],"type":"object"}` | | 400 | Invalid request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 401 | Authentication required | application/json | `{"$ref":"#/components/schemas/Error"}` | | 403 | Permission denied | application/json | `{"$ref":"#/components/schemas/Error"}` | | 404 | Resource not found | application/json | `{"$ref":"#/components/schemas/Error"}` | | 409 | Conflicting request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 413 | Request body too large | application/json | `{"$ref":"#/components/schemas/Error"}` | | 422 | Version unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | | 429 | Limit reached | application/json | `{"$ref":"#/components/schemas/Error"}` | | 500 | Unexpected failure | application/json | `{"$ref":"#/components/schemas/Error"}` | | 503 | Dependency unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | ## listEnvironments `GET /v1/environments` **Parameters** | Name | Location | Required | Schema | | --- | --- | --- | --- | | `limit` | query | No | `{"default":50,"maximum":100,"minimum":1,"type":"integer"}` | | `cursor` | query | No | `{"maxLength":2048,"type":"string"}` | | `workspaceId` | query | No | `{"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$","type":"string"}` | | `status` | query | No | `{"items":{"$ref":"#/components/schemas/EnvironmentStatus"},"maxItems":5,"type":"array"}` | | `snapshotId` | query | No | `{"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$","type":"string"}` | | `search` | query | No | `{"maxLength":120,"type":"string"}` | | `label` | query | No | `{"items":{"pattern":"^[^=]+=.*$","type":"string"},"maxItems":32,"type":"array"}` | **Responses** | Status | Meaning | Content type | Schema | | --- | --- | --- | --- | | 200 | The workspace's environments, newest first | application/json | `{"properties":{"data":{"items":{"$ref":"#/components/schemas/Environment"},"type":"array"},"nextCursor":{"type":["string","null"]}},"required":["data","nextCursor"],"type":"object"}` | | 400 | Invalid request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 401 | Authentication required | application/json | `{"$ref":"#/components/schemas/Error"}` | | 403 | Permission denied | application/json | `{"$ref":"#/components/schemas/Error"}` | | 404 | Resource not found | application/json | `{"$ref":"#/components/schemas/Error"}` | | 409 | Conflicting request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 413 | Request body too large | application/json | `{"$ref":"#/components/schemas/Error"}` | | 422 | Version unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | | 429 | Limit reached | application/json | `{"$ref":"#/components/schemas/Error"}` | | 500 | Unexpected failure | application/json | `{"$ref":"#/components/schemas/Error"}` | | 503 | Dependency unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | ## createEnvironment `POST /v1/environments` **Parameters** | Name | Location | Required | Schema | | --- | --- | --- | --- | | `idempotency-key` | header | No | `{"maxLength":128,"minLength":8,"pattern":"^[A-Za-z0-9._:-]+$","type":"string"}` | **Request body** Request body required: Yes. `application/json` ```json { "properties": { "apps": { "items": { "anyOf": [ { "pattern": "^[a-z][a-z0-9-]{1,39}$", "type": "string" }, { "additionalProperties": false, "properties": { "app": { "pattern": "^[a-z][a-z0-9-]{1,39}$", "type": "string" }, "name": { "pattern": "^[a-z][a-z0-9-]{0,39}$", "type": "string" }, "settings": { "additionalProperties": {}, "type": "object" } }, "required": [ "app" ], "type": "object" } ] }, "maxItems": 10, "minItems": 1, "type": "array" }, "labels": { "additionalProperties": { "maxLength": 256, "type": "string" }, "default": {}, "type": "object" }, "name": { "maxLength": 120, "minLength": 1, "type": [ "string", "null" ] }, "snapshotId": { "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", "type": "string" }, "timeLimitMinutes": { "maximum": 1440, "minimum": 1, "type": "integer" }, "tokenInUrl": { "default": false, "type": "boolean" }, "workspaceId": { "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", "type": "string" } }, "type": "object" } ``` **Responses** | Status | Meaning | Content type | Schema | | --- | --- | --- | --- | | 200 | The environment an earlier request with this Idempotency-Key created, as it is now | application/json | `{"$ref":"#/components/schemas/Environment"}` | | 202 | Environment starting | application/json | `{"$ref":"#/components/schemas/Environment"}` | | 400 | Invalid request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 401 | Authentication required | application/json | `{"$ref":"#/components/schemas/Error"}` | | 403 | Permission denied | application/json | `{"$ref":"#/components/schemas/Error"}` | | 404 | Resource not found | application/json | `{"$ref":"#/components/schemas/Error"}` | | 409 | Conflicting request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 413 | Request body too large | application/json | `{"$ref":"#/components/schemas/Error"}` | | 422 | Version unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | | 429 | Limit reached | application/json | `{"$ref":"#/components/schemas/Error"}` | | 500 | Unexpected failure | application/json | `{"$ref":"#/components/schemas/Error"}` | | 503 | Dependency unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | ## deleteEnvironment `DELETE /v1/environments/{environmentId}` **Parameters** | Name | Location | Required | Schema | | --- | --- | --- | --- | | `environmentId` | path | Yes | `{"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$","type":"string"}` | | `idempotency-key` | header | No | `{"maxLength":128,"minLength":8,"pattern":"^[A-Za-z0-9._:-]+$","type":"string"}` | **Responses** | Status | Meaning | Content type | Schema | | --- | --- | --- | --- | | 200 | Environment deleted; compute and data are released after the response | application/json | `{"$ref":"#/components/schemas/Environment"}` | | 400 | Invalid request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 401 | Authentication required | application/json | `{"$ref":"#/components/schemas/Error"}` | | 403 | Permission denied | application/json | `{"$ref":"#/components/schemas/Error"}` | | 404 | Resource not found | application/json | `{"$ref":"#/components/schemas/Error"}` | | 409 | Conflicting request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 413 | Request body too large | application/json | `{"$ref":"#/components/schemas/Error"}` | | 422 | Version unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | | 429 | Limit reached | application/json | `{"$ref":"#/components/schemas/Error"}` | | 500 | Unexpected failure | application/json | `{"$ref":"#/components/schemas/Error"}` | | 503 | Dependency unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | ## getEnvironment `GET /v1/environments/{environmentId}` **Parameters** | Name | Location | Required | Schema | | --- | --- | --- | --- | | `environmentId` | path | Yes | `{"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$","type":"string"}` | **Responses** | Status | Meaning | Content type | Schema | | --- | --- | --- | --- | | 200 | Environment | application/json | `{"$ref":"#/components/schemas/Environment"}` | | 400 | Invalid request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 401 | Authentication required | application/json | `{"$ref":"#/components/schemas/Error"}` | | 403 | Permission denied | application/json | `{"$ref":"#/components/schemas/Error"}` | | 404 | Resource not found | application/json | `{"$ref":"#/components/schemas/Error"}` | | 409 | Conflicting request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 413 | Request body too large | application/json | `{"$ref":"#/components/schemas/Error"}` | | 422 | Version unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | | 429 | Limit reached | application/json | `{"$ref":"#/components/schemas/Error"}` | | 500 | Unexpected failure | application/json | `{"$ref":"#/components/schemas/Error"}` | | 503 | Dependency unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | ## updateEnvironment `PATCH /v1/environments/{environmentId}` **Parameters** | Name | Location | Required | Schema | | --- | --- | --- | --- | | `environmentId` | path | Yes | `{"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$","type":"string"}` | **Request body** Request body required: Yes. `application/json` ```json { "properties": { "labels": { "additionalProperties": { "maxLength": 256, "type": "string" }, "type": "object" }, "name": { "maxLength": 120, "minLength": 1, "type": [ "string", "null" ] } }, "type": "object" } ``` **Responses** | Status | Meaning | Content type | Schema | | --- | --- | --- | --- | | 200 | Environment updated | application/json | `{"$ref":"#/components/schemas/Environment"}` | | 400 | Invalid request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 401 | Authentication required | application/json | `{"$ref":"#/components/schemas/Error"}` | | 403 | Permission denied | application/json | `{"$ref":"#/components/schemas/Error"}` | | 404 | Resource not found | application/json | `{"$ref":"#/components/schemas/Error"}` | | 409 | Conflicting request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 413 | Request body too large | application/json | `{"$ref":"#/components/schemas/Error"}` | | 422 | Version unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | | 429 | Limit reached | application/json | `{"$ref":"#/components/schemas/Error"}` | | 500 | Unexpected failure | application/json | `{"$ref":"#/components/schemas/Error"}` | | 503 | Dependency unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | ## createCredentials `POST /v1/environments/{environmentId}/apps/{appName}/credentials` **Parameters** | Name | Location | Required | Schema | | --- | --- | --- | --- | | `environmentId` | path | Yes | `{"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$","type":"string"}` | | `appName` | path | Yes | `{"pattern":"^[a-z][a-z0-9-]{0,39}$","type":"string"}` | **Request body** Request body required: No. `application/json` ```json { "properties": { "actorId": { "maxLength": 255, "minLength": 1, "type": "string" } }, "type": "object" } ``` **Responses** | Status | Meaning | Content type | Schema | | --- | --- | --- | --- | | 200 | How to call one app as one actor; the same tokens each time | application/json | `{"$ref":"#/components/schemas/Credentials"}` | | 400 | Invalid request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 401 | Authentication required | application/json | `{"$ref":"#/components/schemas/Error"}` | | 403 | Permission denied | application/json | `{"$ref":"#/components/schemas/Error"}` | | 404 | Resource not found | application/json | `{"$ref":"#/components/schemas/Error"}` | | 409 | Conflicting request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 413 | Request body too large | application/json | `{"$ref":"#/components/schemas/Error"}` | | 422 | Version unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | | 429 | Limit reached | application/json | `{"$ref":"#/components/schemas/Error"}` | | 500 | Unexpected failure | application/json | `{"$ref":"#/components/schemas/Error"}` | | 503 | Dependency unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | ## pauseEnvironment `POST /v1/environments/{environmentId}/pause` **Parameters** | Name | Location | Required | Schema | | --- | --- | --- | --- | | `environmentId` | path | Yes | `{"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$","type":"string"}` | | `idempotency-key` | header | No | `{"maxLength":128,"minLength":8,"pattern":"^[A-Za-z0-9._:-]+$","type":"string"}` | **Responses** | Status | Meaning | Content type | Schema | | --- | --- | --- | --- | | 200 | Environment paused; the gateway refuses calls from now on | application/json | `{"$ref":"#/components/schemas/Environment"}` | | 400 | Invalid request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 401 | Authentication required | application/json | `{"$ref":"#/components/schemas/Error"}` | | 403 | Permission denied | application/json | `{"$ref":"#/components/schemas/Error"}` | | 404 | Resource not found | application/json | `{"$ref":"#/components/schemas/Error"}` | | 409 | Conflicting request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 413 | Request body too large | application/json | `{"$ref":"#/components/schemas/Error"}` | | 422 | Version unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | | 429 | Limit reached | application/json | `{"$ref":"#/components/schemas/Error"}` | | 500 | Unexpected failure | application/json | `{"$ref":"#/components/schemas/Error"}` | | 503 | Dependency unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | ## resumeEnvironment `POST /v1/environments/{environmentId}/resume` **Parameters** | Name | Location | Required | Schema | | --- | --- | --- | --- | | `environmentId` | path | Yes | `{"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$","type":"string"}` | | `idempotency-key` | header | No | `{"maxLength":128,"minLength":8,"pattern":"^[A-Za-z0-9._:-]+$","type":"string"}` | **Responses** | Status | Meaning | Content type | Schema | | --- | --- | --- | --- | | 200 | Environment starting again | application/json | `{"$ref":"#/components/schemas/Environment"}` | | 400 | Invalid request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 401 | Authentication required | application/json | `{"$ref":"#/components/schemas/Error"}` | | 403 | Permission denied | application/json | `{"$ref":"#/components/schemas/Error"}` | | 404 | Resource not found | application/json | `{"$ref":"#/components/schemas/Error"}` | | 409 | Conflicting request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 413 | Request body too large | application/json | `{"$ref":"#/components/schemas/Error"}` | | 422 | Version unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | | 429 | Limit reached | application/json | `{"$ref":"#/components/schemas/Error"}` | | 500 | Unexpected failure | application/json | `{"$ref":"#/components/schemas/Error"}` | | 503 | Dependency unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | ## getMe `GET /v1/me` **Responses** | Status | Meaning | Content type | Schema | | --- | --- | --- | --- | | 200 | The caller, its organization, permissions and limits | application/json | `{"$ref":"#/components/schemas/Me"}` | | 400 | Invalid request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 401 | Authentication required | application/json | `{"$ref":"#/components/schemas/Error"}` | | 403 | Permission denied | application/json | `{"$ref":"#/components/schemas/Error"}` | | 404 | Resource not found | application/json | `{"$ref":"#/components/schemas/Error"}` | | 409 | Conflicting request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 413 | Request body too large | application/json | `{"$ref":"#/components/schemas/Error"}` | | 422 | Version unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | | 429 | Limit reached | application/json | `{"$ref":"#/components/schemas/Error"}` | | 500 | Unexpected failure | application/json | `{"$ref":"#/components/schemas/Error"}` | | 503 | Dependency unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | ## listRequests `GET /v1/requests` **Parameters** | Name | Location | Required | Schema | | --- | --- | --- | --- | | `limit` | query | No | `{"default":50,"maximum":100,"minimum":1,"type":"integer"}` | | `cursor` | query | No | `{"maxLength":2048,"type":"string"}` | | `environmentId` | query | Yes | `{"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$","type":"string"}` | | `app` | query | No | `{"pattern":"^[a-z][a-z0-9-]{0,39}$","type":"string"}` | | `method` | query | No | `{"enum":["GET","HEAD","POST","PUT","PATCH","DELETE","OPTIONS","CONNECT","TRACE"],"type":"string"}` | | `responseStatus` | query | No | `{"maximum":599,"minimum":100,"type":"integer"}` | | `statusClass` | query | No | `{"enum":["2xx","3xx","4xx","5xx"],"type":"string"}` | | `writesOnly` | query | No | `{"type":"boolean"}` | | `search` | query | No | `{"maxLength":200,"pattern":"^[^\\0]*$","type":"string"}` | | `from` | query | No | `{"format":"date-time","type":"string"}` | | `to` | query | No | `{"format":"date-time","type":"string"}` | **Responses** | Status | Meaning | Content type | Schema | | --- | --- | --- | --- | | 200 | Calls the gateway recorded for an environment, newest first; recording is best effort | application/json | `{"properties":{"data":{"items":{"$ref":"#/components/schemas/Request"},"type":"array"},"nextCursor":{"type":["string","null"]}},"required":["data","nextCursor"],"type":"object"}` | | 400 | Invalid request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 401 | Authentication required | application/json | `{"$ref":"#/components/schemas/Error"}` | | 403 | Permission denied | application/json | `{"$ref":"#/components/schemas/Error"}` | | 404 | Resource not found | application/json | `{"$ref":"#/components/schemas/Error"}` | | 409 | Conflicting request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 413 | Request body too large | application/json | `{"$ref":"#/components/schemas/Error"}` | | 422 | Version unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | | 429 | Limit reached | application/json | `{"$ref":"#/components/schemas/Error"}` | | 500 | Unexpected failure | application/json | `{"$ref":"#/components/schemas/Error"}` | | 503 | Dependency unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | ## getRequestStats `GET /v1/requests/stats` **Parameters** | Name | Location | Required | Schema | | --- | --- | --- | --- | | `environmentId` | query | Yes | `{"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$","type":"string"}` | | `app` | query | No | `{"pattern":"^[a-z][a-z0-9-]{0,39}$","type":"string"}` | | `method` | query | No | `{"enum":["GET","HEAD","POST","PUT","PATCH","DELETE","OPTIONS","CONNECT","TRACE"],"type":"string"}` | | `responseStatus` | query | No | `{"maximum":599,"minimum":100,"type":"integer"}` | | `statusClass` | query | No | `{"enum":["2xx","3xx","4xx","5xx"],"type":"string"}` | | `writesOnly` | query | No | `{"type":"boolean"}` | | `search` | query | No | `{"maxLength":200,"pattern":"^[^\\0]*$","type":"string"}` | | `from` | query | Yes | `{"format":"date-time","type":"string"}` | | `to` | query | Yes | `{"format":"date-time","type":"string"}` | **Responses** | Status | Meaning | Content type | Schema | | --- | --- | --- | --- | | 200 | Counts and latency of the recorded calls in a window | application/json | `{"$ref":"#/components/schemas/RequestStats"}` | | 400 | Invalid request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 401 | Authentication required | application/json | `{"$ref":"#/components/schemas/Error"}` | | 403 | Permission denied | application/json | `{"$ref":"#/components/schemas/Error"}` | | 404 | Resource not found | application/json | `{"$ref":"#/components/schemas/Error"}` | | 409 | Conflicting request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 413 | Request body too large | application/json | `{"$ref":"#/components/schemas/Error"}` | | 422 | Version unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | | 429 | Limit reached | application/json | `{"$ref":"#/components/schemas/Error"}` | | 500 | Unexpected failure | application/json | `{"$ref":"#/components/schemas/Error"}` | | 503 | Dependency unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | ## getRequest `GET /v1/requests/{requestId}` **Parameters** | Name | Location | Required | Schema | | --- | --- | --- | --- | | `requestId` | path | Yes | `{"pattern":"^req_[0-9a-z]{16}$","type":"string"}` | **Responses** | Status | Meaning | Content type | Schema | | --- | --- | --- | --- | | 200 | A recorded call with its headers and body previews | application/json | `{"$ref":"#/components/schemas/RequestDetail"}` | | 400 | Invalid request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 401 | Authentication required | application/json | `{"$ref":"#/components/schemas/Error"}` | | 403 | Permission denied | application/json | `{"$ref":"#/components/schemas/Error"}` | | 404 | Resource not found | application/json | `{"$ref":"#/components/schemas/Error"}` | | 409 | Conflicting request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 413 | Request body too large | application/json | `{"$ref":"#/components/schemas/Error"}` | | 422 | Version unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | | 429 | Limit reached | application/json | `{"$ref":"#/components/schemas/Error"}` | | 500 | Unexpected failure | application/json | `{"$ref":"#/components/schemas/Error"}` | | 503 | Dependency unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | ## getRequestBody `GET /v1/requests/{requestId}/body/{direction}` **Parameters** | Name | Location | Required | Schema | | --- | --- | --- | --- | | `requestId` | path | Yes | `{"pattern":"^req_[0-9a-z]{16}$","type":"string"}` | | `direction` | path | Yes | `{"enum":["request","response"],"type":"string"}` | **Responses** | Status | Meaning | Content type | Schema | | --- | --- | --- | --- | | 200 | The recorded body as an attachment | application/octet-stream | `{"format":"binary","type":"string"}` | | 400 | Invalid request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 401 | Authentication required | application/json | `{"$ref":"#/components/schemas/Error"}` | | 403 | Permission denied | application/json | `{"$ref":"#/components/schemas/Error"}` | | 404 | Resource not found | application/json | `{"$ref":"#/components/schemas/Error"}` | | 409 | Conflicting request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 413 | Request body too large | application/json | `{"$ref":"#/components/schemas/Error"}` | | 422 | Version unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | | 429 | Limit reached | application/json | `{"$ref":"#/components/schemas/Error"}` | | 500 | Unexpected failure | application/json | `{"$ref":"#/components/schemas/Error"}` | | 503 | Dependency unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | ## listSnapshots `GET /v1/snapshots` **Parameters** | Name | Location | Required | Schema | | --- | --- | --- | --- | | `limit` | query | No | `{"default":50,"maximum":100,"minimum":1,"type":"integer"}` | | `cursor` | query | No | `{"maxLength":2048,"type":"string"}` | | `workspaceId` | query | No | `{"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$","type":"string"}` | | `environmentId` | query | No | `{"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$","type":"string"}` | | `status` | query | No | `{"items":{"$ref":"#/components/schemas/SnapshotStatus"},"maxItems":4,"type":"array"}` | **Responses** | Status | Meaning | Content type | Schema | | --- | --- | --- | --- | | 200 | The workspace's snapshots, newest first | application/json | `{"properties":{"data":{"items":{"$ref":"#/components/schemas/Snapshot"},"type":"array"},"nextCursor":{"type":["string","null"]}},"required":["data","nextCursor"],"type":"object"}` | | 400 | Invalid request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 401 | Authentication required | application/json | `{"$ref":"#/components/schemas/Error"}` | | 403 | Permission denied | application/json | `{"$ref":"#/components/schemas/Error"}` | | 404 | Resource not found | application/json | `{"$ref":"#/components/schemas/Error"}` | | 409 | Conflicting request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 413 | Request body too large | application/json | `{"$ref":"#/components/schemas/Error"}` | | 422 | Version unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | | 429 | Limit reached | application/json | `{"$ref":"#/components/schemas/Error"}` | | 500 | Unexpected failure | application/json | `{"$ref":"#/components/schemas/Error"}` | | 503 | Dependency unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | ## createSnapshot `POST /v1/snapshots` **Parameters** | Name | Location | Required | Schema | | --- | --- | --- | --- | | `idempotency-key` | header | No | `{"maxLength":128,"minLength":8,"pattern":"^[A-Za-z0-9._:-]+$","type":"string"}` | **Request body** Request body required: Yes. `application/json` ```json { "properties": { "environmentId": { "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", "type": "string" }, "name": { "maxLength": 120, "minLength": 1, "type": [ "string", "null" ] } }, "required": [ "environmentId" ], "type": "object" } ``` **Responses** | Status | Meaning | Content type | Schema | | --- | --- | --- | --- | | 200 | The snapshot an earlier request with this Idempotency-Key created, as it is now | application/json | `{"$ref":"#/components/schemas/Snapshot"}` | | 202 | Snapshot created; its status is `saving` until the data is saved | application/json | `{"$ref":"#/components/schemas/Snapshot"}` | | 400 | Invalid request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 401 | Authentication required | application/json | `{"$ref":"#/components/schemas/Error"}` | | 403 | Permission denied | application/json | `{"$ref":"#/components/schemas/Error"}` | | 404 | Resource not found | application/json | `{"$ref":"#/components/schemas/Error"}` | | 409 | Conflicting request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 413 | Request body too large | application/json | `{"$ref":"#/components/schemas/Error"}` | | 422 | Version unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | | 429 | Limit reached | application/json | `{"$ref":"#/components/schemas/Error"}` | | 500 | Unexpected failure | application/json | `{"$ref":"#/components/schemas/Error"}` | | 503 | Dependency unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | ## deleteSnapshot `DELETE /v1/snapshots/{snapshotId}` **Parameters** | Name | Location | Required | Schema | | --- | --- | --- | --- | | `snapshotId` | path | Yes | `{"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$","type":"string"}` | **Responses** | Status | Meaning | Content type | Schema | | --- | --- | --- | --- | | 200 | Snapshot deleted; its files are removed afterwards | application/json | `{"$ref":"#/components/schemas/Snapshot"}` | | 400 | Invalid request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 401 | Authentication required | application/json | `{"$ref":"#/components/schemas/Error"}` | | 403 | Permission denied | application/json | `{"$ref":"#/components/schemas/Error"}` | | 404 | Resource not found | application/json | `{"$ref":"#/components/schemas/Error"}` | | 409 | Conflicting request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 413 | Request body too large | application/json | `{"$ref":"#/components/schemas/Error"}` | | 422 | Version unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | | 429 | Limit reached | application/json | `{"$ref":"#/components/schemas/Error"}` | | 500 | Unexpected failure | application/json | `{"$ref":"#/components/schemas/Error"}` | | 503 | Dependency unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | ## getSnapshot `GET /v1/snapshots/{snapshotId}` **Parameters** | Name | Location | Required | Schema | | --- | --- | --- | --- | | `snapshotId` | path | Yes | `{"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$","type":"string"}` | **Responses** | Status | Meaning | Content type | Schema | | --- | --- | --- | --- | | 200 | Snapshot | application/json | `{"$ref":"#/components/schemas/Snapshot"}` | | 400 | Invalid request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 401 | Authentication required | application/json | `{"$ref":"#/components/schemas/Error"}` | | 403 | Permission denied | application/json | `{"$ref":"#/components/schemas/Error"}` | | 404 | Resource not found | application/json | `{"$ref":"#/components/schemas/Error"}` | | 409 | Conflicting request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 413 | Request body too large | application/json | `{"$ref":"#/components/schemas/Error"}` | | 422 | Version unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | | 429 | Limit reached | application/json | `{"$ref":"#/components/schemas/Error"}` | | 500 | Unexpected failure | application/json | `{"$ref":"#/components/schemas/Error"}` | | 503 | Dependency unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | ## updateSnapshot `PATCH /v1/snapshots/{snapshotId}` **Parameters** | Name | Location | Required | Schema | | --- | --- | --- | --- | | `snapshotId` | path | Yes | `{"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$","type":"string"}` | **Request body** Request body required: Yes. `application/json` ```json { "properties": { "name": { "maxLength": 120, "minLength": 1, "type": [ "string", "null" ] } }, "required": [ "name" ], "type": "object" } ``` **Responses** | Status | Meaning | Content type | Schema | | --- | --- | --- | --- | | 200 | Snapshot renamed | application/json | `{"$ref":"#/components/schemas/Snapshot"}` | | 400 | Invalid request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 401 | Authentication required | application/json | `{"$ref":"#/components/schemas/Error"}` | | 403 | Permission denied | application/json | `{"$ref":"#/components/schemas/Error"}` | | 404 | Resource not found | application/json | `{"$ref":"#/components/schemas/Error"}` | | 409 | Conflicting request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 413 | Request body too large | application/json | `{"$ref":"#/components/schemas/Error"}` | | 422 | Version unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | | 429 | Limit reached | application/json | `{"$ref":"#/components/schemas/Error"}` | | 500 | Unexpected failure | application/json | `{"$ref":"#/components/schemas/Error"}` | | 503 | Dependency unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | ## listWorkspaces `GET /v1/workspaces` **Parameters** | Name | Location | Required | Schema | | --- | --- | --- | --- | | `limit` | query | No | `{"default":50,"maximum":100,"minimum":1,"type":"integer"}` | | `cursor` | query | No | `{"maxLength":2048,"type":"string"}` | | `archived` | query | No | `{"default":"false","enum":["true","false"],"type":"string"}` | **Responses** | Status | Meaning | Content type | Schema | | --- | --- | --- | --- | | 200 | The organization's workspaces; an API key sees its own | application/json | `{"properties":{"data":{"items":{"$ref":"#/components/schemas/Workspace"},"type":"array"},"nextCursor":{"type":["string","null"]}},"required":["data","nextCursor"],"type":"object"}` | | 400 | Invalid request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 401 | Authentication required | application/json | `{"$ref":"#/components/schemas/Error"}` | | 403 | Permission denied | application/json | `{"$ref":"#/components/schemas/Error"}` | | 404 | Resource not found | application/json | `{"$ref":"#/components/schemas/Error"}` | | 409 | Conflicting request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 413 | Request body too large | application/json | `{"$ref":"#/components/schemas/Error"}` | | 422 | Version unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | | 429 | Limit reached | application/json | `{"$ref":"#/components/schemas/Error"}` | | 500 | Unexpected failure | application/json | `{"$ref":"#/components/schemas/Error"}` | | 503 | Dependency unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | ## getWorkspace `GET /v1/workspaces/{workspaceId}` **Parameters** | Name | Location | Required | Schema | | --- | --- | --- | --- | | `workspaceId` | path | Yes | `{"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$","type":"string"}` | **Responses** | Status | Meaning | Content type | Schema | | --- | --- | --- | --- | | 200 | Workspace | application/json | `{"$ref":"#/components/schemas/Workspace"}` | | 400 | Invalid request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 401 | Authentication required | application/json | `{"$ref":"#/components/schemas/Error"}` | | 403 | Permission denied | application/json | `{"$ref":"#/components/schemas/Error"}` | | 404 | Resource not found | application/json | `{"$ref":"#/components/schemas/Error"}` | | 409 | Conflicting request | application/json | `{"$ref":"#/components/schemas/Error"}` | | 413 | Request body too large | application/json | `{"$ref":"#/components/schemas/Error"}` | | 422 | Version unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | | 429 | Limit reached | application/json | `{"$ref":"#/components/schemas/Error"}` | | 500 | Unexpected failure | application/json | `{"$ref":"#/components/schemas/Error"}` | | 503 | Dependency unavailable | application/json | `{"$ref":"#/components/schemas/Error"}` | --- # Diagnose authentication and access errors Separate a missing key, insufficient permissions, workspace scope, and app actor access. First identify which endpoint failed. A management API key authenticates calls to State Machines. Credentials authenticate calls to an app. An API key does not work at an app URL, and app credentials do not work at the management API. ## Check the management key If construction throws `Pass apiKey or set STATEMACHINES_API_KEY`, check that the variable is present in the process that runs the code. The SDK does not load `.env` files. In Cloudflare Workers, pass `apiKey` explicitly. For an HTTP `401` with `unauthenticated`, check whether the key is missing, expired, or revoked. Replace it through the dashboard if needed. Do not print the key while diagnosing the failure. For `403` with `forbidden`, inspect the current permissions through `sm.me()` and identify the operation that failed. Creating an environment requires `environments:write`, waiting for its status requires `environments:read`, and obtaining app credentials requires `environments:connect`. If the failed call manages members, invitations, workspaces, or API keys, check the caller type. An API key gets `403 forbidden` on these operations, even when it holds every environment permission. Sign in as a member with the required permission. API keys cannot create API keys. If a dashboard action is unavailable, check both permission and environment status. **Copy prompt** needs a running environment. Pause is available on running environments. Resume is available on paused or failed environments. Ask an organization administrator for missing management permissions. ## Confirm the workspace An API key belongs to one workspace. Changing the selected workspace in your browser does not change the key's scope. A `404` with `not_found` can mean the resource is unavailable in the key's workspace. Compare the resource's workspace with the key's workspace returned by `sm.me()`. For member access tokens, confirm the `workspaceId` passed to the SDK. Do not recreate data until you have checked that the original resource is in the expected workspace. ## Check app credentials separately For a request to the app URL, use the URL and headers returned by `connect()`. [Credentials and actors](/environments/credentials/) lists the headers and actor authentication each API needs. If authentication succeeds but a Salesforce operation is denied, confirm the selected actor and its permissions. A State Machines organization role is not a Salesforce actor role. Revoking an API key does not end access to app data. See [revocation and app access](/environments/access/#revocation-and-app-access-have-different-effects) for what revocation ends and what it leaves running. ## Recover dashboard access If the dashboard shows a sign-in prompt, sign in again before retrying the action. If a workspace or resource link is unavailable, confirm the selected organization and workspace. A resource from another organization can appear missing even when its ID is correct. An archived workspace refuses new environments and snapshots, and changes to their names or labels. For `workspace_archived`, switch to an active workspace for a new resource or ask a member with `workspaces:write` to unarchive the original workspace. Existing environment pause, resume, and deletion remain available with the required permission. If a key was created but its value is no longer visible, revoke that key and create another. Refreshing the list or replaying creation cannot recover the secret. --- # Diagnose environment startup failures Read the resource state and failure before retrying creation, resume, or a wait. When creation or resume fails, read the environment's current status and failure before trying another create call. An environment can exist even when the caller did not receive a successful response. ## Read startup failures `EnvironmentFailedError` carries `error.environment`. Its `failure` identifies the app, code, and message when available. For `invalid_settings`, fix the named setting instead of retrying unchanged input. If the environment never ran, State Machines deletes it with reason `failed`. If an environment previously ran and later fails to start or restart, it becomes `failed` and keeps its data for one day. A failed environment can be resumed, subject to the underlying failure and its remaining lifetime. ## Resolve creation errors | Error | Next step | | --- | --- | | `version_unavailable` | Check the app ID and its available versions. The app needs a default version. | | `environment_limit_reached` | Inspect starting and running environments. Pause or delete ones your task owns and no longer needs. | | `invalid_request` | Read `fields` for the rejected input paths. | | `invalid_status` | Read the resource's current status before choosing the next action. | | `idempotency_key_reused` | The key was used with a different input. Replay the exact original input, or use a new key for a new environment. | | `workspace_archived` | Use an active workspace for creation, or unarchive the original workspace. Existing environment lifecycle actions remain available. | If the dashboard has no matching environments, clear its status and creator filters before deciding which resources need cleanup. Read your applicable limits with `sm.me()`. Do not assume that another workspace or organization has the same allowance. ## Recover a timed-out wait `WaitTimeoutError` means the SDK reached its wait deadline. It does not prove that startup failed, and it does not delete the environment. The error's `resource` is the last successful read, which can be null. If you retained the environment ID, read it again and either wait for `running` or delete it. With `wait: false`, accept creation first, then call `wait` inside the `try` block whose `finally` deletes the environment. If the create response itself was lost, replay the exact create input with the same idempotency key. Follow [operation recovery](/sdk-api/recovery/) rather than generating a new key. `UnexpectedStatusError` means the requested status requires another action. For example, waiting for a paused environment to become running does not resume it. Call `resume()` if the task still needs it, or delete it when finished. --- # Diagnose failed app requests Distinguish State Machines gateway failures, native app errors, and incomplete request records. `credentials.fetch()` returns an HTTP response for both successful and failed app calls. It does not convert a Salesforce error response into `StateMachinesError`, and it never retries a request. Check the status and response body before assuming the request reached a native app operation. Network failures can reject without a response. ## Identify State Machines errors The app URL can return a State Machines error before the app handles the request. These errors use a JSON `error` object. | Code | Meaning and next step | | --- | --- | | `invalid_request` | The path or a header is not valid. Use the app URL from `connect()`. Do not send `StateMachines-*` headers other than `StateMachines-Environment-Token`. | | `unauthenticated` | Obtain credentials for the exact app and confirm that the environment token reaches the app gateway. | | `not_found` | Check the environment ID and app instance name in the URL. | | `environment_not_running` | Read `error.status`. Resume a paused or failed environment when appropriate. A deleted environment needs a replacement. | | `environment_starting` | Wait for startup before requesting again. | | `environment_unavailable` | The app is restarting or did not answer. Inspect environment status and the `Retry-After` header. | | `capacity_exceeded` | Reduce concurrent requests to the app. | | `unsupported_protocol` | The app does not serve this path or API version, or the request body is compressed. Check the version's `apis` and remove `Content-Encoding`. | | `body_too_large` | The request body is over 20 MiB. Split the operation using a supported native API. | | `response_too_large` | The app response is over 4 MiB or compressed. Request a smaller page: lower `batchSize` in `Sforce-Query-Options` for SOQL, or set `maxRecords` for Bulk results. | | `unavailable` | State Machines or the app could not serve the request. Wait for the `Retry-After` delay, then retry. | | `internal_error` | Retry once, then report the `request-id` response header. | An instruction to retry after a delay does not prove an ambiguous native write had no effect. Reconcile the app state before repeating a write. ## Inspect native app errors Salesforce REST and Bulk errors use their native error arrays. SOAP failures use SOAP faults. OAuth failures use the OAuth error shape. Read the error code for that protocol, rather than parsing every body as a State Machines management error. For field and object failures, inspect the target API version's describe response. For permission failures, verify the actor. For an unsupported operation, check [known differences](/apps/known-differences/). ## Compare recorded requests Use `sm.requests.list({ environmentId })` to inspect recorded calls, newest first. If a call is missing, confirm the environment and app name, clear status and method filters, and include the call's timestamp in the time range. Start a new first page after changing filters. Check `respondedBy` on a matching record. `gateway` means the gateway produced the error response. The app may still have run the request. The gateway can fail after it forwards the request. Refusals from the app runner, such as `capacity_exceeded` and `unsupported_protocol`, are recorded with `respondedBy: 'app'` and a null `errorCode`. `sm.requests.get(id)` adds selected headers and body previews. For a missing body, inspect `coverage`, `reason`, and `encoding` before downloading it. `omitted` means no body was stored. A body download then returns `not_found`. A binary body has no text preview. `previewTruncated` means only the start of a stored text body appears in the detail response. `sm.requests.body(id, 'request')` or `'response'` returns the stored bytes, including any redactions. XML, multipart, and credential-route bodies can be omitted even when the request itself succeeded. `sm.requests.stats({ environmentId, from, to })` summarizes recorded calls over a time range. Pass `from` and `to` as SDK `Date` values. Search requires both ends of the window too. The window can be at most 7 days. Recording is best effort, so missing entries or aggregate counts cannot establish that no request occurred. Capture the environment ID, app version, method, path, response status, and sanitized error when reporting a problem. Exclude credentials and sensitive body data. See [requests and audit events](/environments/observability/) for filters, coverage fields, and statistics. For lifecycle changes rather than app responses, inspect the environment's audit events. --- # Find missing or unexpected data Check environment identity, snapshot selection, app names, actors, and write results. Before you change the query, confirm which environment and app the client reached. Fresh environments and restored environments have different starting data, and actors can see different records. ## Confirm the target Read the environment by ID. Match the app name to the name used at creation. If an environment contains two Salesforce copies, `connect('salesforce')` and `connect('partner-org')` reach different app data. Check that the client uses credentials from this environment. Credentials copied from another test can point at a still-running environment with older records. Obtain fresh credentials through the intended environment's `connect()` helper. ## Check the starting data An environment created with `apps` starts with its app version's predefined data. It does not contain your customer Salesforce records. Creating a snapshot also does not import data from a customer organization. An environment created with `snapshotId` starts from that saved copy. Confirm the snapshot ID, its source, and its status. A descriptive name helps people find a fixture, but the ID determines what is restored. Writes to a restored environment do not update the snapshot. If the fixture needs new records, create a new snapshot after preparing them and update the ID used by tests. ## Verify the write and actor Read the original write response and check `response.ok`. A request that returned `400` did not satisfy the test's success condition. [Credentials and actors](/environments/credentials/#request-behavior) describes how `credentials.fetch()` returns HTTP errors. For a successful write that a later query cannot see, confirm the actor, queried object, record identifier, and query filters. Restricted actors may have different record access than the administrator. A test that changes actors also changes the access being tested. After an ambiguous network failure, inspect records through a stable identifier before repeating the write. A blind retry can create duplicates. ## Account for deletion and expiry A paused or failed environment keeps its app data for one day, then State Machines deletes it. Deleting an environment removes its app data. Management history, including recorded request bodies, is separate. A time limit or automatic expiry can delete the environment without another client call. Inspect status and audit events to understand what happened. A ready snapshot can outlive its source environment. Deleting the snapshot does not erase data in environments already created from it. See [snapshot reuse](/snapshots/reuse/) and [environment lifecycle](/environments/lifecycle/) for their independent ownership. If the concern is a sensitive value in request history, review [recorded data handling](/environments/security/). Deleting the app environment does not erase copies in request records, snapshots, local downloads, or agent transcripts.