@usestatemachines/sdk 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -19
- package/agents/SKILL.md +10 -7
- package/agents/index.js +6 -6
- package/agents/llms.txt +19 -11
- package/agents/prompt.md +1 -1
- package/esm/extensions/client.d.ts +24 -2
- package/esm/extensions/client.js +26 -3
- package/esm/extensions/index.d.ts +2 -2
- package/esm/extensions/running-environment.d.ts +18 -4
- package/esm/extensions/running-environment.js +24 -2
- package/esm/extensions/wait.d.ts +3 -1
- package/esm/extensions/wait.js +17 -2
- package/esm/funcs/environments-get-request-payload.d.ts +11 -0
- package/esm/funcs/environments-get-request-payload.js +100 -0
- package/esm/funcs/environments-get-request-stats.d.ts +11 -0
- package/esm/funcs/environments-get-request-stats.js +103 -0
- package/esm/funcs/environments-get-request.d.ts +11 -0
- package/esm/funcs/environments-get-request.js +96 -0
- package/esm/funcs/environments-list-requests.d.ts +11 -0
- package/esm/funcs/environments-list-requests.js +105 -0
- package/esm/funcs/environments-resume.d.ts +11 -0
- package/esm/funcs/environments-resume.js +93 -0
- package/esm/funcs/environments-stop.js +1 -0
- package/esm/funcs/environments-suspend.d.ts +11 -0
- package/esm/funcs/environments-suspend.js +93 -0
- package/esm/lib/config.d.ts +2 -2
- package/esm/lib/config.js +2 -2
- package/esm/models/environment-app.d.ts +2 -0
- package/esm/models/environment-app.js +2 -0
- package/esm/models/environment-request-detail.d.ts +40 -0
- package/esm/models/environment-request-detail.js +39 -0
- package/esm/models/environment-request-stats.d.ts +26 -0
- package/esm/models/environment-request-stats.js +31 -0
- package/esm/models/environment-request.d.ts +31 -0
- package/esm/models/environment-request.js +34 -0
- package/esm/models/environment-snapshot.d.ts +3 -2
- package/esm/models/environment-snapshot.js +3 -2
- package/esm/models/environment.d.ts +4 -0
- package/esm/models/environment.js +4 -0
- package/esm/models/error-response.d.ts +7 -0
- package/esm/models/error-response.js +7 -0
- package/esm/models/index.d.ts +4 -0
- package/esm/models/index.js +4 -0
- package/esm/models/operations/create-environment-definition.d.ts +10 -0
- package/esm/models/operations/create-environment-definition.js +3 -0
- package/esm/models/operations/get-environment-request-payload.d.ts +23 -0
- package/esm/models/operations/get-environment-request-payload.js +19 -0
- package/esm/models/operations/get-environment-request-stats.d.ts +70 -0
- package/esm/models/operations/get-environment-request-stats.js +55 -0
- package/esm/models/operations/get-environment-request.d.ts +25 -0
- package/esm/models/operations/get-environment-request.js +21 -0
- package/esm/models/operations/index.d.ts +6 -0
- package/esm/models/operations/index.js +6 -0
- package/esm/models/operations/list-environment-requests.d.ts +75 -0
- package/esm/models/operations/list-environment-requests.js +59 -0
- package/esm/models/operations/resume-environment.d.ts +25 -0
- package/esm/models/operations/resume-environment.js +26 -0
- package/esm/models/operations/stop-environment.d.ts +3 -1
- package/esm/models/operations/stop-environment.js +8 -2
- package/esm/models/operations/suspend-environment.d.ts +25 -0
- package/esm/models/operations/suspend-environment.js +26 -0
- package/esm/models/release.d.ts +11 -0
- package/esm/models/release.js +10 -0
- package/esm/models/request-payload-preview.d.ts +31 -0
- package/esm/models/request-payload-preview.js +33 -0
- package/esm/sdk/environments.d.ts +6 -0
- package/esm/sdk/environments.js +24 -0
- package/openapi.json +1380 -29
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @usestatemachines/sdk
|
|
2
2
|
|
|
3
3
|
TypeScript SDK for [State](https://usestatemachines.com): start disposable environments of replica
|
|
4
|
-
apps, connect to them, snapshot their data, and stop them.
|
|
4
|
+
apps, connect to them, snapshot their data, suspend and resume them, and stop them.
|
|
5
5
|
|
|
6
6
|
- Node.js 20.20 or later (22 or 24 recommended), and Cloudflare Workers. ESM only.
|
|
7
7
|
- Retries transient failures safely, including `create`, which is idempotent.
|
|
@@ -34,12 +34,13 @@ It takes `{ readyTimeoutMs, signal }`: the wait gives up after `readyTimeoutMs`
|
|
|
34
34
|
minutes by default). A string app is shorthand for `{ key: s, appId: s }`; nothing else is inferred.
|
|
35
35
|
Salesforce offers several APIs, so pass `apiId`.
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
rethrows the original error. The SDK's errors
|
|
39
|
-
`StateMachinesError`; check with `instanceof`)
|
|
40
|
-
`true` when the stop request succeeded. When
|
|
41
|
-
`stopError` says why, and you should call
|
|
42
|
-
to repeat. The exception is your own key,
|
|
37
|
+
`env.stop()` ends the environment and discards its data. If the wait fails after the environment
|
|
38
|
+
was created, `start()` stops the environment, then rethrows the original error. The SDK's errors
|
|
39
|
+
(`EnvironmentFailedError`, `TimeoutError` or a `StateMachinesError`; check with `instanceof`)
|
|
40
|
+
carry `environmentId` and `stopped`. `stopped` is `true` when the stop request succeeded. When
|
|
41
|
+
it's `false`, the stop wasn't confirmed: `stopError` says why, and you should call
|
|
42
|
+
`sm.environments.stop(err.environmentId)`, which is safe to repeat. The exception is your own key,
|
|
43
|
+
below.
|
|
43
44
|
|
|
44
45
|
When you abort the `signal` during the wait, `start()` still stops the environment. If the stop
|
|
45
46
|
succeeded, it rejects with your `signal.reason`, unchanged. If not, it rejects with a
|
|
@@ -84,6 +85,35 @@ chosen actor's permissions. Without an `actorId`, `connect()` uses the environme
|
|
|
84
85
|
default connection, if it has one, or else the first actor the app lists, the administrator, so
|
|
85
86
|
pick a less-privileged actor to test permission behaviour.
|
|
86
87
|
|
|
88
|
+
## Suspending and resuming
|
|
89
|
+
|
|
90
|
+
Suspend an environment to end its sandboxes and keep its data, then resume it later on the same
|
|
91
|
+
data:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
await env.suspend();
|
|
95
|
+
await env.waitUntilSuspended();
|
|
96
|
+
// Later, from the same handle or with sm.environments.resume(env.id):
|
|
97
|
+
await env.resume();
|
|
98
|
+
await env.waitUntilReady();
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
- `env.suspend()` is `sm.environments.suspend(id)`. The environment reads `suspending`, then
|
|
102
|
+
`suspended`, its apps read `suspended` and `suspendedAt` is set. Only a running environment
|
|
103
|
+
whose apps are all ready can be suspended; anything else answers `409 not-ready`.
|
|
104
|
+
- While suspended, the lifetime is paused: `expiresAt` moves out by the suspended time when it
|
|
105
|
+
resumes, and the environment doesn't count toward the live-environment cap.
|
|
106
|
+
- `resume()` starts it again; it answers `409 not-suspended` unless the environment is
|
|
107
|
+
`suspended` or `failed`, and `429 environment-limit` when the cap is reached. `waitUntilSuspended()` resolves
|
|
108
|
+
on `suspended` and throws `EnvironmentFailedError` if the environment ends instead.
|
|
109
|
+
- Suspending isn't cleanup: a suspended environment keeps its data until you stop it.
|
|
110
|
+
`stop()` interrupts ongoing work, including suspension and snapshot capture.
|
|
111
|
+
|
|
112
|
+
Lifecycle commands require orchestration availability. `stop`, `suspend` and `resume` each
|
|
113
|
+
send a fresh idempotency key and reuse it across automatic retries. You can pass your own
|
|
114
|
+
`{ idempotencyKey }`. Failures carry that key so you can retry the same command after a timeout;
|
|
115
|
+
a timeout does not establish whether the command was accepted.
|
|
116
|
+
|
|
87
117
|
## Full control
|
|
88
118
|
|
|
89
119
|
Use `create`, `waitUntilReady` and `connect` directly when you want to run each step yourself, or
|
|
@@ -116,18 +146,19 @@ try {
|
|
|
116
146
|
|
|
117
147
|
1. `create(input)` returns one `Environment` as soon as the API accepts it, before the apps boot.
|
|
118
148
|
2. `waitUntilReady(id)` polls until it is `running`. It throws `EnvironmentFailedError` if the
|
|
119
|
-
environment becomes `failed`, `stopping` or `stopped`, and
|
|
120
|
-
(default 10 minutes). It **never stops the environment**,
|
|
149
|
+
environment becomes `failed`, `suspending`, `suspended`, `stopping` or `stopped`, and
|
|
150
|
+
`TimeoutError` after `timeoutMs` (default 10 minutes). It **never stops the environment**,
|
|
151
|
+
even when it throws.
|
|
121
152
|
3. `connect(id, appKey, { apiId, actorId })` returns a `Connection` to one app (see
|
|
122
153
|
[Connections](#connections)).
|
|
123
154
|
4. `stop(id)` returns as soon as the API accepts it, and a second call also resolves.
|
|
124
|
-
`waitUntilStopped(id)` resolves only when the environment is `stopped`
|
|
155
|
+
`waitUntilStopped(id)` resolves only when the environment is `stopped` **and**
|
|
125
156
|
every app's `disposal` is `complete`. If any disposal is `failed` or `unknown`, it throws
|
|
126
157
|
`DisposalIncompleteError` with the environment.
|
|
127
158
|
|
|
128
|
-
`lifetimeMinutes` counts from when the environment starts, so boot time uses part of it
|
|
129
|
-
runs out, the environment stops with `stopReason: 'expired'`. The
|
|
130
|
-
`(await sm.credential.get()).limits.environmentLifetime`.
|
|
159
|
+
`lifetimeMinutes` counts from when the environment starts, so boot time uses part of it; time spent
|
|
160
|
+
suspended doesn't count. When it runs out, the environment stops with `stopReason: 'expired'`. The
|
|
161
|
+
default and maximum are in `(await sm.credential.get()).limits.environmentLifetime`.
|
|
131
162
|
|
|
132
163
|
Both waits accept `{ timeoutMs, signal }`. Aborting the `signal` rejects the wait with
|
|
133
164
|
`signal.reason`. They poll at growing intervals of about 1 to 5 seconds, with random jitter, and
|
|
@@ -169,8 +200,10 @@ Pass `snapshotId` instead of `apps` to `start()` or `create()`. Each app keeps t
|
|
|
169
200
|
captured; `snapshotAppInstanceKeys` restores only some of them. The new environment's
|
|
170
201
|
`sourceEnvironmentSnapshotId` and each app's `sourceAppInstanceSnapshotId` name the source.
|
|
171
202
|
|
|
172
|
-
-
|
|
173
|
-
across apps
|
|
203
|
+
- Every app is paused while the snapshot is cut, so a multi-app snapshot records one instant
|
|
204
|
+
across apps (`consistency: 'environment'`). A single app, or a capture whose pause was refused,
|
|
205
|
+
is `'app_instance'`. It can only be taken while the environment is `running` and each app is
|
|
206
|
+
`ready`.
|
|
174
207
|
- `readyTimeoutMs` bounds the wait for `ready` (10 minutes by default), as in `start()`. While the
|
|
175
208
|
API is busy (429 `snapshot-limit`), the capture is retried with the same idempotency key for up
|
|
176
209
|
to 60 seconds.
|
|
@@ -241,8 +274,8 @@ attempts:
|
|
|
241
274
|
| --- | --- | --- |
|
|
242
275
|
| `502`, `503`, `504` | Yes | The API is saturated (`503`), or Cloudflare can't reach it for a moment (`502`, `504`). |
|
|
243
276
|
| No response: connection error or attempt timeout | Yes | The request may never have arrived. |
|
|
244
|
-
| `429` (`environment-limit`) | No | A quota on live environments.
|
|
245
|
-
| `429` (`snapshot-limit`) | Only by `snapshots.capture()` and `env.snapshot()`, for up to 60 seconds |
|
|
277
|
+
| `429` (`environment-limit`) | No | A quota on live environments. Suspend or stop one, then create again. |
|
|
278
|
+
| `429` (`snapshot-limit`) | Only by `snapshots.capture()` and `env.snapshot()`, for up to 60 seconds | Sent by older APIs when too many captures ran. The current API answers `409 capture-in-progress` instead. |
|
|
246
279
|
| Any other status: other `4xx`, and other `5xx` | No | Not retried by the SDK. |
|
|
247
280
|
|
|
248
281
|
- A `Retry-After` header is honoured, up to 5 seconds.
|
|
@@ -334,7 +367,8 @@ created also carry `snapshotId` and `idempotencyKey`.
|
|
|
334
367
|
|
|
335
368
|
| Code | Status | Meaning |
|
|
336
369
|
| --- | --- | --- |
|
|
337
|
-
| `
|
|
370
|
+
| `capture-in-progress` | 409 | This environment is already capturing a snapshot; one runs at a time. Wait for it (`waitUntilReady`), then capture again. |
|
|
371
|
+
| `snapshot-limit` | 429 | Older APIs only: too many captures were running. `capture` retries it for up to 60 seconds; `create` doesn't. |
|
|
338
372
|
| `not-ready` | 409 | Capture: the environment isn't `running` or an app isn't `ready`. Restore: the snapshot isn't `ready` yet. Delete: the snapshot is still `capturing`. |
|
|
339
373
|
| `version-conflict` | 409 | `rename` sent a stale `expectedVersion`, or the snapshot was deleted. `get` it: if `deletedAt` is set it's gone, otherwise retry with its `version`. |
|
|
340
374
|
| `release-unavailable` | 422 | Restore: no release on this deployment matches the snapshot's apps. |
|
|
@@ -346,7 +380,7 @@ try {
|
|
|
346
380
|
await sm.environments.create(input);
|
|
347
381
|
} catch (error) {
|
|
348
382
|
if (error instanceof StateMachinesError && error.code === ErrorCode.EnvironmentLimit) {
|
|
349
|
-
console.error('Too many live environments; stop one first.');
|
|
383
|
+
console.error('Too many live environments; suspend or stop one first.');
|
|
350
384
|
} else {
|
|
351
385
|
throw error;
|
|
352
386
|
}
|
|
@@ -433,6 +467,30 @@ const response = await fetch(new URL('services/data/v67.0/limits', salesforce.ba
|
|
|
433
467
|
});
|
|
434
468
|
```
|
|
435
469
|
|
|
470
|
+
## Recorded requests
|
|
471
|
+
|
|
472
|
+
Request history is best effort. Missing records do not mean that no requests ran.
|
|
473
|
+
Stored records remain readable after an environment stops.
|
|
474
|
+
|
|
475
|
+
```ts
|
|
476
|
+
const stats = await sm.environments.getRequestStats(env.id, {
|
|
477
|
+
from: new Date(Date.now() - 60 * 60_000),
|
|
478
|
+
to: new Date(),
|
|
479
|
+
});
|
|
480
|
+
console.log(stats.count, stats.p95Ms);
|
|
481
|
+
const page = await sm.environments.listRequests(env.id, { appKey: 'salesforce', limit: 20 });
|
|
482
|
+
const first = page.data[0];
|
|
483
|
+
if (first) {
|
|
484
|
+
const detail = await sm.environments.getRequest(env.id, first.id);
|
|
485
|
+
console.log(detail.status, detail.responseBody.preview);
|
|
486
|
+
const body = await sm.environments.getRequestPayload(env.id, first.id, 'response');
|
|
487
|
+
const bytes = await new Response(body).arrayBuffer();
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Payload coverage reports credential redaction and omissions. The detail preview
|
|
492
|
+
is bounded; the payload method returns the full stored body as a byte stream.
|
|
493
|
+
|
|
436
494
|
## Supported runtimes
|
|
437
495
|
|
|
438
496
|
- **Node.js 20.20 or later**, with 22 or 24 recommended: Node 20 is past its end of life, and
|
|
@@ -449,6 +507,14 @@ TypeScript consumers need `lib: ["DOM"]` or `@types/node`, for the `fetch`, `Req
|
|
|
449
507
|
version.
|
|
450
508
|
- The API may add error codes and enum values, such as a new status or stop reason. Enums are
|
|
451
509
|
open: handle values you don't recognise.
|
|
510
|
+
- 0.5.0 removes database history and fork metadata. See [What's new in 0.5.0](#whats-new-in-050).
|
|
511
|
+
- 0.4.0 adds suspend and resume and keeps every 0.3.0 call: `stop` still ends the environment.
|
|
512
|
+
`Environment` gains `suspendedAt` and `sourceEnvironmentId`, so object literals typed as
|
|
513
|
+
`Environment` need them. A snapshot's `consistency` is now `'environment'`, `'app_instance'`
|
|
514
|
+
or `null` while it isn't ready (it was always `'per-app-instance'`). See
|
|
515
|
+
[What's new in 0.4.0](#whats-new-in-040).
|
|
516
|
+
- Upgrade from 0.3.0 to capture snapshots: the API now answers `consistency: null` while a
|
|
517
|
+
capture runs, which 0.3.0 rejects as an invalid response even though the capture started.
|
|
452
518
|
- 0.3.0 adds the snapshot APIs and keeps every 0.2.0 call. An input with neither `apps` nor
|
|
453
519
|
`snapshotId` now fails with `Pass apps or snapshotId.` `Environment` and its apps gain
|
|
454
520
|
`sourceEnvironmentSnapshotId` and `sourceAppInstanceSnapshotId` (`null` unless restored), so
|
|
@@ -472,6 +538,43 @@ TypeScript consumers need `lib: ["DOM"]` or `@types/node`, for the `fetch`, `Req
|
|
|
472
538
|
releases don't declare a default actor yet.
|
|
473
539
|
- `await using` isn't supported. Stop the environment in `finally`.
|
|
474
540
|
|
|
541
|
+
## What's new in 0.5.0
|
|
542
|
+
|
|
543
|
+
### Added
|
|
544
|
+
|
|
545
|
+
- `sm.environments.listRequests`, `getRequest`, `getRequestPayload` and `getRequestStats`
|
|
546
|
+
read best-effort gateway request history, including after the environment stops.
|
|
547
|
+
- A failed environment retains its application data and can be resumed or stopped.
|
|
548
|
+
Stop takes priority over startup, recovery and snapshot capture.
|
|
549
|
+
|
|
550
|
+
### Removed
|
|
551
|
+
|
|
552
|
+
- The generated `listEnvironmentHistory`, `getEnvironmentPosition` and
|
|
553
|
+
`getEnvironmentExchange` operations and their history/position/exchange models.
|
|
554
|
+
Use the request-history methods above for recorded replica traffic.
|
|
555
|
+
- `EnvironmentApp.forkedAt`. Database object history and point-in-time forks are no longer
|
|
556
|
+
part of the API.
|
|
557
|
+
|
|
558
|
+
`waitUntilStopped()` now requires `stopped` and completed disposal. A retained `failed`
|
|
559
|
+
environment is not stopped. Lifecycle commands require Temporal availability; existing
|
|
560
|
+
replica traffic does not.
|
|
561
|
+
|
|
562
|
+
## What's new in 0.4.0
|
|
563
|
+
|
|
564
|
+
- `sm.environments.suspend(id)` ends an environment's sandboxes and keeps its data;
|
|
565
|
+
`sm.environments.resume(id)` runs it again on that data, and
|
|
566
|
+
`sm.environments.waitUntilSuspended(id)` waits for the suspension (see
|
|
567
|
+
[Suspending and resuming](#suspending-and-resuming)).
|
|
568
|
+
- `RunningEnvironment` gains `suspend()`, `resume()`, `waitUntilSuspended()` and
|
|
569
|
+
`waitUntilReady()`.
|
|
570
|
+
- `stop` is unchanged: it ends the environment and discards its data, suspended or not.
|
|
571
|
+
- New statuses: `suspending` and `suspended` for environments, `suspended` and `recovering` for
|
|
572
|
+
apps. New fields: `suspendedAt` and `sourceEnvironmentId` on an environment, `forkedAt` on an
|
|
573
|
+
app. `waitUntilReady()` throws `EnvironmentFailedError` on `suspending` and `suspended`.
|
|
574
|
+
- Snapshots: `consistency` is `'environment'` (every app paused together), `'app_instance'` (each
|
|
575
|
+
app captured on its own) or `null` until ready. One capture runs per environment at a time; a
|
|
576
|
+
second gets `409 capture-in-progress`.
|
|
577
|
+
|
|
475
578
|
## What's new in 0.3.0
|
|
476
579
|
|
|
477
580
|
- `sm.snapshots`: `capture`, `create`, `waitUntilReady`, `get`, `list`, `rename` and `delete`
|
package/agents/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: state-machines
|
|
3
|
-
description: Use when an agent or test needs a disposable replica of a SaaS app such as Salesforce, to run integration tests or try API calls against a sandboxed copy instead of a real org. Covers the @usestatemachines/sdk TypeScript SDK - starting environments, choosing an actor, snapshots, cleanup, and errors such as environment-limit and release-unavailable. Also use when the user mentions State Machines, STATE_API_KEY or @usestatemachines/sdk.
|
|
3
|
+
description: Use when an agent or test needs a disposable replica of a SaaS app such as Salesforce, to run integration tests or try API calls against a sandboxed copy instead of a real org. Covers the @usestatemachines/sdk TypeScript SDK - starting environments, choosing an actor, snapshots, suspending and resuming, cleanup, and errors such as environment-limit and release-unavailable. Also use when the user mentions State Machines, STATE_API_KEY or @usestatemachines/sdk.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Read `node_modules/@usestatemachines/sdk/README.md` before relying on memory; it matches the installed version. If the package is missing, run `npm install @usestatemachines/sdk`.
|
|
@@ -12,7 +12,7 @@ Read `node_modules/@usestatemachines/sdk/README.md` before relying on memory; it
|
|
|
12
12
|
3. Open a `try` right after `start()` returns, and call `env.stop()` in its `finally`.
|
|
13
13
|
4. Connect through one API profile. Salesforce offers several and has no default, so pass `apiId`: `env.connect('salesforce', { apiId: 'rest' })`. This acts as the environment's declared default connection's actor, or else the app's first actor, the administrator. To act with another user's permissions, choose one from `await env.actors('salesforce')` and pass its `id` as `actorId`. Salesforce actor IDs are its user IDs, such as `005…`, never a role name, so read them instead of hard-coding one.
|
|
14
14
|
5. Call the app with the connection's `fetch`. A leading `/` means the root of `baseUrl`.
|
|
15
|
-
6. In `finally`, `await env.stop()
|
|
15
|
+
6. In `finally`, `await env.stop()`; it ends the environment and discards its data. In CI, then `await env.waitUntilStopped()`.
|
|
16
16
|
|
|
17
17
|
The whole flow, as the package ships it. It is ESM with top-level `await`: save it as `state-quickstart.mts` and run `npx tsx state-quickstart.mts`.
|
|
18
18
|
|
|
@@ -34,9 +34,10 @@ try {
|
|
|
34
34
|
## Rules
|
|
35
35
|
|
|
36
36
|
- Always stop in `finally`. `waitUntilStopped` confirms every app was disposed; use it in CI so a run fails when cleanup can't be confirmed.
|
|
37
|
+
- `env.suspend()` ends the sandboxes and keeps the data, with the lifetime paused; it is not cleanup. Await `env.waitUntilSuspended()` before `env.resume()`, then `env.waitUntilReady()` before connecting again. A suspended environment still needs `env.stop()`.
|
|
37
38
|
- To keep a failed environment for inspection, use the full-control path: `sm.environments.create`, then `sm.environments.waitUntilReady(environment.id)`, then `sm.environments.connect(environment.id, key, options)`. Nothing on that path stops the environment for you.
|
|
38
39
|
- Lifetime counts from the start, including boot. It defaults to 15 minutes, or the deployment maximum if that is lower: `(await sm.credential.get()).limits.environmentLifetime`. Set `lifetimeMinutes` in `start` or `create` for longer runs.
|
|
39
|
-
- Live environments are capped per organization: `limits.liveEnvironments` from `sm.credential.get()`. Stop the ones you no longer need.
|
|
40
|
+
- Live environments are capped per organization: `limits.liveEnvironments` from `sm.credential.get()`. Stop the ones you no longer need; suspended ones don't count until they resume.
|
|
40
41
|
- Never print, log or commit `STATE_API_KEY`, tokens, or `headers()` or `actorToken()` output.
|
|
41
42
|
- Salesforce's SOAP profile (`soap-partner`) takes the actor token in the envelope, not a header: put `connection.actorToken()` in `SessionHeader/sessionId`. For it, `headers()` carries only `X-State-Environment-Token`.
|
|
42
43
|
- `connection.fetch` never sends your API key, never follows redirects and only reaches URLs under `baseUrl`.
|
|
@@ -56,18 +57,20 @@ The SDK already retries `502`, `503`, `504` and connection errors with one idemp
|
|
|
56
57
|
| --- | --- | --- |
|
|
57
58
|
| `release-unavailable`, 422 | Unknown or retired `appId` or `appReleaseId`. A string app is used as the `appId`. | Check the IDs against `sm.catalog.get()`. Omit `appReleaseId` for the default release. |
|
|
58
59
|
| `release-unavailable`, 503, or a failed app with that `errorCode` | This deployment doesn't serve the release. The SDK retries a 503 within a 10-minute budget. | Omit `appReleaseId`. If it persists, report the `requestId`. Pass a `signal` to bound the wait. |
|
|
59
|
-
| `environment-limit`, 429 | The organization is at its live-environment cap. Not retried. | `sm.environments.list({ status: 'live' })`, stop one you no longer need, then start again. |
|
|
60
|
+
| `environment-limit`, 429 | The organization is at its live-environment cap. Not retried. | `sm.environments.list({ status: 'live' })`, stop or suspend one you no longer need, then start again. |
|
|
61
|
+
| `not-suspended` or `suspend-in-progress`, 409 | `resume()` before the environment finished suspending, or `stop()` while it suspends. | Await `env.waitUntilSuspended()` first. |
|
|
60
62
|
| `lifetime-too-long`, 422 | `lifetimeMinutes` is above the maximum. | Use at most `limits.environmentLifetime.maximumMinutes`. |
|
|
61
63
|
| `TypeError: Unknown actor <id> for <appKey>. Choose one of: <ids>.` | The `actorId` isn't one of the app's actors. No token was revealed. | Pass an `id` from `await env.actors(appKey)`, or omit `actorId` to act as the default actor. |
|
|
62
64
|
| `TypeError: Choose one API for <appKey>: <ids>.` | The app has several API profiles and `apiId` is missing or unknown. Salesforce always needs one. | Pass `apiId` from the listed IDs, such as `rest`. |
|
|
63
65
|
| `start()` rejects with `EnvironmentFailedError` or `TimeoutError` | The environment failed, or wasn't `running` within `readyTimeoutMs` (10 minutes by default). If `err.stopped` is `true`, `start()` already stopped it. | Read `err.environment` (`null` on a `TimeoutError` that saw no read): its `stopReason` and each app's `errorCode`. If `err.stopped` is `false`, read `err.stopError` and call `sm.environments.stop(err.environmentId)`. To keep a failed environment, use the full-control path. |
|
|
64
66
|
| `StateMachinesError` (`status` null) for invalid `apps`: `Pass at least one app.`, `App key "<key>" is used twice.` or `"<s>" can't be used as an app key; pass { key, appId } instead.` | Empty list, duplicate key, or a string that isn't a valid key. No request was sent. | Pass one to ten apps with unique keys; use `{ key, appId }` when the ID isn't a valid key. |
|
|
65
67
|
| `TypeError: App ... is not ready to connect` | On the full-control path, connect() ran before the app was `ready`. | Await `waitUntilReady` first, or use `start()`. |
|
|
66
|
-
| `connection-unavailable`, 409 | The app stopped, or the environment is no longer `running`. | Check `sm.environments.get(env.id)`; if it stopped or expired, start a new one. |
|
|
67
|
-
| `EnvironmentFailedError` from `waitUntilReady` | On the full-control path, the environment became `failed`, `stopping` or `stopped`. | Read `err.environment.stopReason` and each app's `errorCode`. Still stop it in `finally`. |
|
|
68
|
+
| `connection-unavailable`, 409 | The app stopped, or the environment is no longer `running`. | Check `sm.environments.get(env.id)`; if it is suspended, resume it; if it stopped or expired, start a new one. |
|
|
69
|
+
| `EnvironmentFailedError` from `waitUntilReady` | On the full-control path, the environment became `failed`, `suspending`, `suspended`, `stopping` or `stopped`. | Read `err.environment.status`, its `stopReason` and each app's `errorCode`. Still stop it in `finally`. |
|
|
68
70
|
| A vendor client such as jsforce is rejected by the gateway | The gateway needs `X-State-Environment-Token` on every request; jsforce only sends it with custom headers configured. | Prefer `connection.fetch`. If you pass `headers()` to another client, send them only to `baseUrl` and disable redirects. |
|
|
69
71
|
| `not-ready`, 409 | Capture: the environment isn't `running` or an app isn't `ready`. Restore: the snapshot isn't `ready` yet. Delete: the snapshot is still `capturing`. | Capture from the handle `start()` returns. Before a restore or delete, `await sm.snapshots.waitUntilReady(snapshotId)`. |
|
|
70
|
-
| `
|
|
72
|
+
| `capture-in-progress`, 409 | This environment is already capturing; one capture runs at a time. | `await sm.snapshots.waitUntilReady(<the running snapshot's id>)`, then capture again. |
|
|
73
|
+
| `snapshot-limit`, 429 | Older APIs only: too many captures were running. `capture` and `env.snapshot()` already retried it for 60 seconds. | Wait, then capture again. |
|
|
71
74
|
| `SnapshotFailedError` | The capture failed. `err.snapshot.errorCode` is `capture-failed`, or `capture-interrupted` when the API restarted mid-capture. The snapshot is kept. | Capture again with a new idempotency key (the default); the old key returns the same failed snapshot. |
|
|
72
75
|
| `SnapshotTimeoutError` | The snapshot wasn't `ready` within `readyTimeoutMs`. The capture continues on the server. | `await sm.snapshots.waitUntilReady(err.snapshotId)`. |
|
|
73
76
|
| `forbidden`, 403, from `sm.snapshots.delete` | The key lacks `snapshots:delete`. Keys created by members never hold it. | Use a key whose creator holds `snapshots:delete`, or keep the snapshot. |
|
package/agents/index.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
// Generated by scripts/build-agents.mjs from templates/ and examples/quickstart.ts.
|
|
2
2
|
|
|
3
3
|
export const prompt =
|
|
4
|
-
"Set up State Machines so this project can run code against a disposable replica of a SaaS app, such as Salesforce. Follow these steps in order.\n\n1. Install the SDK: `npm install @usestatemachines/sdk`. It is ESM only and runs on Node.js 20.20 or later and on Cloudflare Workers, not in browsers.\n2. Check that `STATE_API_KEY` is set without printing it: `test -n \"$STATE_API_KEY\"`. If it is missing, stop and ask me to set it; the SDK doesn't read `.env` files. In Cloudflare Workers, pass the key as `new StateMachines({ apiKey })`.\n3. Save this quickstart unchanged as `state-quickstart.mts` and run `npx tsx state-quickstart.mts` to confirm the setup. It prints `200` and the org's limits:\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATE_API_KEY\nconst env = await sm.environments.start({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce', { apiId: 'rest' });\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.stop();\n}\n```\n\n4. Adapt it to my task. Log `await sm.catalog.get()` to find each app's ID and the actors and API profiles its release offers; don't guess IDs. Start only the apps the task needs; a string such as `'salesforce'` means `{ key: 'salesforce', appId: 'salesforce' }`. Salesforce always needs an `apiId`. `connect()` acts as the declared default connection's actor, or else the app's first actor, the administrator; to act as a specific user, pass an `actorId` from `await env.actors('salesforce')`. To reuse seeded data, `await env.snapshot()` once, then `sm.environments.start({ snapshotId })`.\n5. Always call `env.stop()` in a `finally
|
|
4
|
+
"Set up State Machines so this project can run code against a disposable replica of a SaaS app, such as Salesforce. Follow these steps in order.\n\n1. Install the SDK: `npm install @usestatemachines/sdk`. It is ESM only and runs on Node.js 20.20 or later and on Cloudflare Workers, not in browsers.\n2. Check that `STATE_API_KEY` is set without printing it: `test -n \"$STATE_API_KEY\"`. If it is missing, stop and ask me to set it; the SDK doesn't read `.env` files. In Cloudflare Workers, pass the key as `new StateMachines({ apiKey })`.\n3. Save this quickstart unchanged as `state-quickstart.mts` and run `npx tsx state-quickstart.mts` to confirm the setup. It prints `200` and the org's limits:\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATE_API_KEY\nconst env = await sm.environments.start({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce', { apiId: 'rest' });\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.stop();\n}\n```\n\n4. Adapt it to my task. Log `await sm.catalog.get()` to find each app's ID and the actors and API profiles its release offers; don't guess IDs. Start only the apps the task needs; a string such as `'salesforce'` means `{ key: 'salesforce', appId: 'salesforce' }`. Salesforce always needs an `apiId`. `connect()` acts as the declared default connection's actor, or else the app's first actor, the administrator; to act as a specific user, pass an `actorId` from `await env.actors('salesforce')`. To reuse seeded data, `await env.snapshot()` once, then `sm.environments.start({ snapshotId })`.\n5. Always call `env.stop()` in a `finally`; it ends the environment and discards its data. In CI, then await `env.waitUntilStopped()` so the run fails if cleanup isn't confirmed. If `start()` fails after creating the environment, it stops it; an `EnvironmentFailedError` or `TimeoutError` carries `err.environmentId` and `err.stopped`. If the create itself fails, call `sm.environments.start(input, { idempotencyKey: err.idempotencyKey })` with the same `input`; never retry without the key, which can start a second environment.\n6. Never print, log or commit keys or tokens, including `connection.headers()` and `connection.actorToken()`, which return live tokens.\n\nRead more: `node_modules/@usestatemachines/sdk/README.md`, https://state-web.usestatemachines.workers.dev/llms.txt and https://www.npmjs.com/package/@usestatemachines/sdk\n";
|
|
5
5
|
|
|
6
6
|
export const llmsTxt =
|
|
7
|
-
"# State Machines\n\n> State Machines runs disposable environments of replica SaaS apps, such as Salesforce, for tests and coding agents. Start, connect to and stop them with the TypeScript SDK `@usestatemachines/sdk`.\n\nInstall with `npm install @usestatemachines/sdk`. It is ESM only and runs on Node.js 20.20 or later (22 or 24 recommended) and Cloudflare Workers, not browsers. TypeScript needs `lib: [\"DOM\"]` or `@types/node`. `new StateMachines()` reads the API key from `STATE_API_KEY` and doesn't read `.env` files; in Cloudflare Workers, pass `apiKey` explicitly. Never print the key. The installed `node_modules/@usestatemachines/sdk/README.md` matches your version: read it before relying on memory.\n\n## Concepts\n\n- Environment: one disposable run of one or more apps. `const env = await sm.environments.start({ apps: ['salesforce'] })` creates it, waits until it is `running` and returns a `RunningEnvironment`. If the environment fails or the wait times out or is aborted, `start()` stops the environment and rethrows the original error; an `EnvironmentFailedError` or `TimeoutError` carries `err.environmentId` and `err.stopped`. Always call `env.stop()` in a `finally`.\n- Apps: a string is shorthand for `{ key: s, appId: s }`, nothing more. Use the object form, such as `{ key: 'crm', appId: 'salesforce' }`, for your own key or two instances of one app. `appId` names the app in the catalog (`sm.catalog.get()`); `key` is your name for it in this environment, and later calls take the key.\n- Actor: one of the app's users; requests run with that user's permissions. Salesforce actor IDs are its user IDs, such as `005…`, never a role name. `await env.actors('salesforce')` lists them in the release's order. connect() acts as the environment's declared default connection's actor, or else the first actor, the administrator, unless you pass `actorId`; pick a less-privileged actor to test permissions.\n- API profile: one API of the app, with its path and versions. Salesforce offers several (`rest`, `soap-partner`, `bulk2`, `oauth`) and has no default, so always pass `apiId`: `env.connect('salesforce', { apiId: 'rest' })`.\n- Connection: `connection.fetch` never sends your API key, never follows redirects and never retries. `connection.headers()` returns live tokens: treat them as secrets. Salesforce's SOAP profile (`soap-partner`) takes the actor token in the envelope instead: put `connection.actorToken()`, also a secret, in `SessionHeader/sessionId`; `headers()` then carries only `X-State-Environment-Token`.\n- Full control: `sm.environments.create(input)` returns one `Environment` before the apps boot, `sm.environments.waitUntilReady(environment.id)` waits, and `sm.environments.connect(environment.id, key, options)` connects. Nothing on this path stops the environment for you; use it to keep a failed environment for inspection.\n- Snapshot: the saved data of a running environment's apps, taken per app instance. `const snapshot = await env.snapshot({ name: 'seeded' })` captures it and waits until it is `ready`. `sm.environments.start({ snapshotId: snapshot.id })` starts a new environment from it, in place of `apps`; each app keeps its key. Capturing needs `snapshots:create` and `snapshots:read`, restoring needs `snapshots:read`, `rename` needs `snapshots:create`, and `sm.snapshots.delete(id)` needs `snapshots:delete`, which keys created by members don't hold. Snapshots are never deleted automatically, not even failed or timed-out ones: `SnapshotFailedError` and `SnapshotTimeoutError` carry `err.snapshotId`. `sm.snapshots` also has `create`, `waitUntilReady`, `get`, `list` and `rename`.\n- Vendor clients: the gateway needs `X-State-Environment-Token` on every request. A client such as jsforce only sends it if you configure custom headers. Prefer `connection.fetch`; if you pass `headers()` to another client, send them only to `connection.baseUrl` and disable redirects.\n- Pagination: `environments.list`, `environments.events`, `workspaces.events` and `snapshots.list` return one page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`.\n\n## Statuses\n\nEnums are open: handle values you don't recognise.\n\n- Environment `status`: `queued`, `starting`, `running`, `stopping`, `stopped`, `failed`. `stopReason`: `requested`, `expired`, `app_failed`, `owner_lost`.\n- App `status`: `pending`, `starting`, `ready`, `stopping`, `stopped`, `failed`. A failed app carries `errorCode`.\n- App `disposal`: `pending`, `complete`, `failed`, `unknown`.\n- Snapshot `status`: `capturing`, `ready`, `failed`. A failed snapshot carries `errorCode`: `capture-failed`, or `capture-interrupted` when the API restarted mid-capture.\n- `start()` and `waitUntilReady` throw `EnvironmentFailedError` if the environment becomes `failed`, `stopping` or `stopped`, and `TimeoutError` after 10 minutes by default (`readyTimeoutMs` for `start()`, `timeoutMs` for `waitUntilReady`). `start()` then stops the environment; `waitUntilReady` never does.\n- `waitUntilStopped` resolves when the environment is `stopped` or `failed` and every app's disposal is `complete`. It throws `DisposalIncompleteError` if a disposal is `failed` or `unknown`, and `TimeoutError` if disposal stays `pending`.\n\n## Limits\n\n- Live environments are capped per organization: `(await sm.credential.get()).limits.liveEnvironments`.\n- `lifetimeMinutes` defaults to 15 minutes, or the deployment maximum if that is lower. Both are in `(await sm.credential.get()).limits.environmentLifetime`. Lifetime counts from the start, so boot uses part of it; at the end the environment stops with `stopReason: 'expired'`.\n- Always call `env.stop()` in a `finally`. In CI, then await `env.waitUntilStopped()`.\n\n## Errors and retries\n\nA failed API call throws `StateMachinesError` with `status`, `code`, `requestId` and `retryAfter`. The SDK retries `502`, `503`, `504`, connection errors and attempt timeouts itself, within a 10-minute retry budget (the last attempt can end later); `create` retries reuse one idempotency key. It retries nothing else: not `429`, other `4xx` or other `5xx`. The one exception is snapshot capture, which retries `429` `snapshot-limit` for up to 60 seconds with one idempotency key. `err.stopped === false` with `err.stopError` means the stop wasn't confirmed; `sm.environments.stop(err.environmentId)` is safe to repeat. Without `stopError`, you passed your own `idempotencyKey`, so `start()` left the environment running because another `start()` with that key may be using it; stop it only once nothing else needs it. Every error from `create`, including the create step of `start()`, carries `err.idempotencyKey`: `sm.environments.start(input, { idempotencyKey: err.idempotencyKey })` with the same input returns the environment the first call created. Invalid `apps` fail before any request as a `StateMachinesError` (`status` null, `cause` a `TypeError`; don't replay it) with one of: `Pass at least one app.`, `App key \"<key>\" is used twice.` or `\"<s>\" can't be used as an app key; pass { key, appId } instead.`\n\n| Code | Status | What to do |\n| --- | --- | --- |\n| `environment-limit` | 429 | Not retried. Stop a live environment (`sm.environments.list({ status: 'live' })`), then start again. |\n| `lifetime-too-long` | 422 | Lower `lifetimeMinutes` to at most `limits.environmentLifetime.maximumMinutes`. |\n| `release-unavailable` | 422 | Unknown or retired app or release. Check `appId` and `appReleaseId` against `sm.catalog.get()`. |\n| `release-unavailable` | 503 | This deployment doesn't serve the release. Retried within the budget; omit `appReleaseId` to use the default release, or contact support with the `requestId`. A failed app can carry it as `errorCode`. |\n| `connection-unavailable` | 409 | The app stopped or the environment is no longer `running`. If it stopped or expired, start a new one. (Before revealing any token, connect() throws a `TypeError` for an app that isn't ready, an unknown actor, or a missing `apiId`.) |\n| `idempotency-conflict` | 409 | The idempotency key was used with a different input. Replay with the exact same input. |\n| `snapshot-limit` | 429 | Too many captures are running. `capture` and `env.snapshot()` retry it for up to 60 seconds; if it still fails, wait and capture again. |\n| `not-ready` | 409 | Capture: the environment isn't `running` or an app isn't `ready`. Restore: the snapshot isn't `ready` yet; `await sm.snapshots.waitUntilReady(id)`. Delete: the snapshot is still `capturing`. |\n| `version-conflict` | 409 | `rename` sent a stale `expectedVersion`, or the snapshot was deleted. `get` it: if `deletedAt` is set it's gone, otherwise retry with its `version`. |\n| `release-unavailable` | 422 | On a restore: no release on this deployment matches the snapshot's apps. Start from `apps`, seed again and capture a new snapshot. |\n| `request-timeout` | 503 | Retried automatically. If `create` still fails, replay it with `err.idempotencyKey`. |\n\n## Quickstart\n\nSave it unchanged as `state-quickstart.mts` and run `npx tsx state-quickstart.mts`.\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATE_API_KEY\nconst env = await sm.environments.start({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce', { apiId: 'rest' });\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.stop();\n}\n```\n\n## Docs\n\n- [npm package](https://www.npmjs.com/package/@usestatemachines/sdk): install, README and versions\n- [Agent skill](https://state-web.usestatemachines.workers.dev/skill.md): workflow, rules and troubleshooting for coding agents\n- [OpenAPI document](https://cdn.jsdelivr.net/npm/@usestatemachines/sdk/openapi.json): the HTTP API the SDK calls\n- [State Machines](https://usestatemachines.com): product site\n";
|
|
7
|
+
"# State Machines\n\n> State Machines runs disposable environments of replica SaaS apps, such as Salesforce, for tests and coding agents. Start, connect to, suspend, resume and stop them with the TypeScript SDK `@usestatemachines/sdk`.\n\nInstall with `npm install @usestatemachines/sdk`. It is ESM only and runs on Node.js 20.20 or later (22 or 24 recommended) and Cloudflare Workers, not browsers. TypeScript needs `lib: [\"DOM\"]` or `@types/node`. `new StateMachines()` reads the API key from `STATE_API_KEY` and doesn't read `.env` files; in Cloudflare Workers, pass `apiKey` explicitly. Never print the key. The installed `node_modules/@usestatemachines/sdk/README.md` matches your version: read it before relying on memory.\n\n## Concepts\n\n- Environment: one disposable run of one or more apps. `const env = await sm.environments.start({ apps: ['salesforce'] })` creates it, waits until it is `running` and returns a `RunningEnvironment`. If the environment fails or the wait times out or is aborted, `start()` stops the environment and rethrows the original error; an `EnvironmentFailedError` or `TimeoutError` carries `err.environmentId` and `err.stopped`. Always call `env.stop()` in a `finally`.\n- Suspend and resume: `env.suspend()` ends the sandboxes and keeps the data; the lifetime pauses. Await `env.waitUntilSuspended()`, later `env.resume()` and `env.waitUntilReady()` to run again on the same data. `env.stop()` ends the environment and discards its data; a suspended environment still needs it.\n- Apps: a string is shorthand for `{ key: s, appId: s }`, nothing more. Use the object form, such as `{ key: 'crm', appId: 'salesforce' }`, for your own key or two instances of one app. `appId` names the app in the catalog (`sm.catalog.get()`); `key` is your name for it in this environment, and later calls take the key.\n- Actor: one of the app's users; requests run with that user's permissions. Salesforce actor IDs are its user IDs, such as `005…`, never a role name. `await env.actors('salesforce')` lists them in the release's order. connect() acts as the environment's declared default connection's actor, or else the first actor, the administrator, unless you pass `actorId`; pick a less-privileged actor to test permissions.\n- API profile: one API of the app, with its path and versions. Salesforce offers several (`rest`, `soap-partner`, `bulk2`, `oauth`) and has no default, so always pass `apiId`: `env.connect('salesforce', { apiId: 'rest' })`.\n- Connection: `connection.fetch` never sends your API key, never follows redirects and never retries. `connection.headers()` returns live tokens: treat them as secrets. Salesforce's SOAP profile (`soap-partner`) takes the actor token in the envelope instead: put `connection.actorToken()`, also a secret, in `SessionHeader/sessionId`; `headers()` then carries only `X-State-Environment-Token`.\n- Full control: `sm.environments.create(input)` returns one `Environment` before the apps boot, `sm.environments.waitUntilReady(environment.id)` waits, and `sm.environments.connect(environment.id, key, options)` connects. Nothing on this path stops the environment for you; use it to keep a failed environment for inspection.\n- Snapshot: the saved data of a running environment's apps. Every app is paused while it is cut, so a multi-app snapshot is one instant (`consistency: 'environment'`); a single app is `'app_instance'`. `const snapshot = await env.snapshot({ name: 'seeded' })` captures it and waits until it is `ready`. `sm.environments.start({ snapshotId: snapshot.id })` starts a new environment from it, in place of `apps`; each app keeps its key. Capturing needs `snapshots:create` and `snapshots:read`, restoring needs `snapshots:read`, `rename` needs `snapshots:create`, and `sm.snapshots.delete(id)` needs `snapshots:delete`, which keys created by members don't hold. Snapshots are never deleted automatically, not even failed or timed-out ones: `SnapshotFailedError` and `SnapshotTimeoutError` carry `err.snapshotId`. `sm.snapshots` also has `create`, `waitUntilReady`, `get`, `list` and `rename`.\n- Recorded requests: `sm.environments.listRequests(env.id)` pages the calls made to the environment's apps (method, path, status, duration); `getRequest(env.id, id)` adds headers and bodies. Recording is best effort and SOAP bodies are not stored.\n- Vendor clients: the gateway needs `X-State-Environment-Token` on every request. A client such as jsforce only sends it if you configure custom headers. Prefer `connection.fetch`; if you pass `headers()` to another client, send them only to `connection.baseUrl` and disable redirects.\n- Pagination: `environments.list`, `environments.events`, `workspaces.events` and `snapshots.list` return one page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`.\n\n## Statuses\n\nEnums are open: handle values you don't recognise.\n\n- Environment `status`: `queued`, `starting`, `running`, `suspending`, `suspended`, `stopping`, `stopped`, `failed`. `stopReason`: `requested`, `expired`, `app_failed`, `owner_lost`. A suspended environment keeps its data and carries `suspendedAt`.\n- App `status`: `pending`, `starting`, `ready`, `recovering`, `suspended`, `stopping`, `stopped`, `failed`. A failed app carries `errorCode`.\n- App `disposal`: `pending`, `complete`, `failed`, `unknown`.\n- Snapshot `status`: `capturing`, `ready`, `failed`. A failed snapshot carries `errorCode`: `capture-failed`, `pause-failed` or `pause-lost` when the apps could not be held paused for the capture, or `capture-interrupted` when the API restarted mid-capture.\n- `start()` and `waitUntilReady` throw `EnvironmentFailedError` if the environment becomes `failed`, `suspending`, `suspended`, `stopping` or `stopped`, and `TimeoutError` after 10 minutes by default (`readyTimeoutMs` for `start()`, `timeoutMs` for `waitUntilReady`). `start()` then stops the environment; `waitUntilReady` never does.\n- `waitUntilSuspended` resolves when the environment is `suspended`, and throws `EnvironmentFailedError` if it ends instead.\n- `waitUntilStopped` resolves when the environment is `stopped` and every app's disposal is `complete`. A `failed` environment keeps its data for `resume()` and is not stopped until you call `stop()`. It throws `DisposalIncompleteError` if a disposal is `failed` or `unknown`, and `TimeoutError` if disposal stays `pending`.\n\n## Limits\n\n- Live environments are capped per organization: `(await sm.credential.get()).limits.liveEnvironments`.\n- `lifetimeMinutes` defaults to 15 minutes, or the deployment maximum if that is lower. Both are in `(await sm.credential.get()).limits.environmentLifetime`. Lifetime counts from the start, so boot uses part of it, and pauses while suspended; at the end the environment stops with `stopReason: 'expired'`.\n- A suspended environment doesn't count toward the live cap; resuming it does.\n- Always call `env.stop()` in a `finally`. In CI, then await `env.waitUntilStopped()`.\n\n## Errors and retries\n\nA failed API call throws `StateMachinesError` with `status`, `code`, `requestId` and `retryAfter`. The SDK retries `502`, `503`, `504`, connection errors and attempt timeouts itself, within a 10-minute retry budget (the last attempt can end later); `create` retries reuse one idempotency key. It retries nothing else: not `429`, other `4xx` or other `5xx`. The one exception is snapshot capture, which retries `429` `snapshot-limit` for up to 60 seconds with one idempotency key. `err.stopped === false` with `err.stopError` means the stop wasn't confirmed; `sm.environments.stop(err.environmentId)` is safe to repeat. Without `stopError`, you passed your own `idempotencyKey`, so `start()` left the environment running because another `start()` with that key may be using it; stop it only once nothing else needs it. Every error from `create`, including the create step of `start()`, carries `err.idempotencyKey`: `sm.environments.start(input, { idempotencyKey: err.idempotencyKey })` with the same input returns the environment the first call created. Invalid `apps` fail before any request as a `StateMachinesError` (`status` null, `cause` a `TypeError`; don't replay it) with one of: `Pass at least one app.`, `App key \"<key>\" is used twice.` or `\"<s>\" can't be used as an app key; pass { key, appId } instead.`\n\n| Code | Status | What to do |\n| --- | --- | --- |\n| `environment-limit` | 429 | Not retried. Suspend or stop a live environment (`sm.environments.list({ status: 'live' })`), then start or resume again. |\n| `not-ready` | 409 | Only a running environment whose apps are all ready can be suspended. Wait for it, or stop it. |\n| `not-suspended` | 409 | Only a suspended environment can resume. After `suspend()`, await `waitUntilSuspended()` first. |\n| `suspend-in-progress` | 409 | The environment is suspending. Await `waitUntilSuspended()`, then stop it. |\n| `lifetime-too-long` | 422 | Lower `lifetimeMinutes` to at most `limits.environmentLifetime.maximumMinutes`. |\n| `release-unavailable` | 422 | Unknown or retired app or release. Check `appId` and `appReleaseId` against `sm.catalog.get()`. |\n| `release-unavailable` | 503 | This deployment doesn't serve the release. Retried within the budget; omit `appReleaseId` to use the default release, or contact support with the `requestId`. A failed app can carry it as `errorCode`. |\n| `connection-unavailable` | 409 | The app stopped or the environment is no longer `running`. If it is suspended, resume it; if it stopped or expired, start a new one. (Before revealing any token, connect() throws a `TypeError` for an app that isn't ready, an unknown actor, or a missing `apiId`.) |\n| `idempotency-conflict` | 409 | The idempotency key was used with a different input. Replay with the exact same input. |\n| `capture-in-progress` | 409 | This environment is already capturing a snapshot; one runs at a time. Wait for it, then capture again. |\n| `snapshot-limit` | 429 | Older APIs only. `capture` and `env.snapshot()` retry it for up to 60 seconds; if it still fails, wait and capture again. |\n| `not-ready` | 409 | Capture: the environment isn't `running` or an app isn't `ready`. Restore: the snapshot isn't `ready` yet; `await sm.snapshots.waitUntilReady(id)`. Delete: the snapshot is still `capturing`. |\n| `version-conflict` | 409 | `rename` sent a stale `expectedVersion`, or the snapshot was deleted. `get` it: if `deletedAt` is set it's gone, otherwise retry with its `version`. |\n| `release-unavailable` | 422 | On a restore: no release on this deployment matches the snapshot's apps. Start from `apps`, seed again and capture a new snapshot. |\n| `request-timeout` | 503 | Retried automatically. If `create` still fails, replay it with `err.idempotencyKey`. |\n\n## Quickstart\n\nSave it unchanged as `state-quickstart.mts` and run `npx tsx state-quickstart.mts`.\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATE_API_KEY\nconst env = await sm.environments.start({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce', { apiId: 'rest' });\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.stop();\n}\n```\n\n## Docs\n\n- [npm package](https://www.npmjs.com/package/@usestatemachines/sdk): install, README and versions\n- [Agent skill](https://state-web.usestatemachines.workers.dev/skill.md): workflow, rules and troubleshooting for coding agents\n- [OpenAPI document](https://cdn.jsdelivr.net/npm/@usestatemachines/sdk/openapi.json): the HTTP API the SDK calls\n- [State Machines](https://usestatemachines.com): product site\n";
|
|
8
8
|
|
|
9
9
|
export const skill =
|
|
10
|
-
"---\nname: state-machines\ndescription: Use when an agent or test needs a disposable replica of a SaaS app such as Salesforce, to run integration tests or try API calls against a sandboxed copy instead of a real org. Covers the @usestatemachines/sdk TypeScript SDK - starting environments, choosing an actor, snapshots, cleanup, and errors such as environment-limit and release-unavailable. Also use when the user mentions State Machines, STATE_API_KEY or @usestatemachines/sdk.\n---\n\nRead `node_modules/@usestatemachines/sdk/README.md` before relying on memory; it matches the installed version. If the package is missing, run `npm install @usestatemachines/sdk`.\n\n## Workflow\n\n1. Check that `STATE_API_KEY` is set without printing it (`test -n \"$STATE_API_KEY\"`). If it isn't, ask the user to set it; the SDK doesn't read `.env` files.\n2. Start the environment with only the apps the task needs: `const env = await sm.environments.start({ apps: ['salesforce'] })`. Log `await sm.catalog.get()` for each app's ID and the actors and API profiles its release offers; don't guess IDs. A string app means `{ key: s, appId: s }`; use `{ key, appId }` for your own key. `start()` waits until the environment is `running`. If that fails, it stops the environment and rethrows; an `EnvironmentFailedError` or `TimeoutError` carries `err.environmentId` and `err.stopped`.\n3. Open a `try` right after `start()` returns, and call `env.stop()` in its `finally`.\n4. Connect through one API profile. Salesforce offers several and has no default, so pass `apiId`: `env.connect('salesforce', { apiId: 'rest' })`. This acts as the environment's declared default connection's actor, or else the app's first actor, the administrator. To act with another user's permissions, choose one from `await env.actors('salesforce')` and pass its `id` as `actorId`. Salesforce actor IDs are its user IDs, such as `005…`, never a role name, so read them instead of hard-coding one.\n5. Call the app with the connection's `fetch`. A leading `/` means the root of `baseUrl`.\n6. In `finally`, `await env.stop()`. In CI, then `await env.waitUntilStopped()`.\n\nThe whole flow, as the package ships it. It is ESM with top-level `await`: save it as `state-quickstart.mts` and run `npx tsx state-quickstart.mts`.\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATE_API_KEY\nconst env = await sm.environments.start({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce', { apiId: 'rest' });\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.stop();\n}\n```\n\n## Rules\n\n- Always stop in `finally`. `waitUntilStopped` confirms every app was disposed; use it in CI so a run fails when cleanup can't be confirmed.\n- To keep a failed environment for inspection, use the full-control path: `sm.environments.create`, then `sm.environments.waitUntilReady(environment.id)`, then `sm.environments.connect(environment.id, key, options)`. Nothing on that path stops the environment for you.\n- Lifetime counts from the start, including boot. It defaults to 15 minutes, or the deployment maximum if that is lower: `(await sm.credential.get()).limits.environmentLifetime`. Set `lifetimeMinutes` in `start` or `create` for longer runs.\n- Live environments are capped per organization: `limits.liveEnvironments` from `sm.credential.get()`. Stop the ones you no longer need.\n- Never print, log or commit `STATE_API_KEY`, tokens, or `headers()` or `actorToken()` output.\n- Salesforce's SOAP profile (`soap-partner`) takes the actor token in the envelope, not a header: put `connection.actorToken()` in `SessionHeader/sessionId`. For it, `headers()` carries only `X-State-Environment-Token`.\n- `connection.fetch` never sends your API key, never follows redirects and only reaches URLs under `baseUrl`.\n- Runtimes: Node.js 20.20 or later (22 or 24 recommended), and Cloudflare Workers. ESM only. TypeScript needs `lib: [\"DOM\"]` or `@types/node`. Never ship an API key to a browser.\n- In Cloudflare Workers, pass the key explicitly: `new StateMachines({ apiKey: env.STATE_API_KEY })`, and bound calls with `{ signal: AbortSignal.timeout(20_000) }`, since retries can take minutes.\n- `environments.list`, `environments.events`, `workspaces.events` and `snapshots.list` return one page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`.\n- To reuse seeded data, `await env.snapshot({ name })` once, then `sm.environments.start({ snapshotId })` in place of `apps`. Capturing needs `snapshots:create` and `snapshots:read`. Snapshots are never deleted automatically, not even failed ones.\n- Check errors with `instanceof` (`StateMachinesError`, `EnvironmentFailedError`, `DisposalIncompleteError`, `TimeoutError`, `SnapshotFailedError`, `SnapshotTimeoutError`), not by `name`.\n\n## Recovering a failed create\n\nThe SDK already retries `502`, `503`, `504` and connection errors with one idempotency key. If `start()` or `create` still throws with `err.idempotencyKey`, the create failed: don't call it again as a new request, which can start a second environment. Call `sm.environments.start(input, { idempotencyKey: err.idempotencyKey })` with the same `input`. Don't replay when `err.status` is `null` and `err.cause` is a `TypeError`: the input was invalid and nothing was sent; fix the input instead. A key only recovers a failed create: don't reuse the key of a `start()` whose wait failed, because the replay returns that stopped environment. With your own key, `start()` doesn't stop the environment on failure, because another `start()` with that key may be using it: the error carries `err.environmentId`, `err.stopped` is `false` and there is no `err.stopError`. Stop it only once nothing else needs it. It returns the environment the first call created, or creates it if that call never arrived. A different input fails with `idempotency-conflict`. Keys are kept for a day.\n\n## Troubleshooting\n\n| Symptom | Cause | Fix |\n| --- | --- | --- |\n| `release-unavailable`, 422 | Unknown or retired `appId` or `appReleaseId`. A string app is used as the `appId`. | Check the IDs against `sm.catalog.get()`. Omit `appReleaseId` for the default release. |\n| `release-unavailable`, 503, or a failed app with that `errorCode` | This deployment doesn't serve the release. The SDK retries a 503 within a 10-minute budget. | Omit `appReleaseId`. If it persists, report the `requestId`. Pass a `signal` to bound the wait. |\n| `environment-limit`, 429 | The organization is at its live-environment cap. Not retried. | `sm.environments.list({ status: 'live' })`, stop one you no longer need, then start again. |\n| `lifetime-too-long`, 422 | `lifetimeMinutes` is above the maximum. | Use at most `limits.environmentLifetime.maximumMinutes`. |\n| `TypeError: Unknown actor <id> for <appKey>. Choose one of: <ids>.` | The `actorId` isn't one of the app's actors. No token was revealed. | Pass an `id` from `await env.actors(appKey)`, or omit `actorId` to act as the default actor. |\n| `TypeError: Choose one API for <appKey>: <ids>.` | The app has several API profiles and `apiId` is missing or unknown. Salesforce always needs one. | Pass `apiId` from the listed IDs, such as `rest`. |\n| `start()` rejects with `EnvironmentFailedError` or `TimeoutError` | The environment failed, or wasn't `running` within `readyTimeoutMs` (10 minutes by default). If `err.stopped` is `true`, `start()` already stopped it. | Read `err.environment` (`null` on a `TimeoutError` that saw no read): its `stopReason` and each app's `errorCode`. If `err.stopped` is `false`, read `err.stopError` and call `sm.environments.stop(err.environmentId)`. To keep a failed environment, use the full-control path. |\n| `StateMachinesError` (`status` null) for invalid `apps`: `Pass at least one app.`, `App key \"<key>\" is used twice.` or `\"<s>\" can't be used as an app key; pass { key, appId } instead.` | Empty list, duplicate key, or a string that isn't a valid key. No request was sent. | Pass one to ten apps with unique keys; use `{ key, appId }` when the ID isn't a valid key. |\n| `TypeError: App ... is not ready to connect` | On the full-control path, connect() ran before the app was `ready`. | Await `waitUntilReady` first, or use `start()`. |\n| `connection-unavailable`, 409 | The app stopped, or the environment is no longer `running`. | Check `sm.environments.get(env.id)`; if it stopped or expired, start a new one. |\n| `EnvironmentFailedError` from `waitUntilReady` | On the full-control path, the environment became `failed`, `stopping` or `stopped`. | Read `err.environment.stopReason` and each app's `errorCode`. Still stop it in `finally`. |\n| A vendor client such as jsforce is rejected by the gateway | The gateway needs `X-State-Environment-Token` on every request; jsforce only sends it with custom headers configured. | Prefer `connection.fetch`. If you pass `headers()` to another client, send them only to `baseUrl` and disable redirects. |\n| `not-ready`, 409 | Capture: the environment isn't `running` or an app isn't `ready`. Restore: the snapshot isn't `ready` yet. Delete: the snapshot is still `capturing`. | Capture from the handle `start()` returns. Before a restore or delete, `await sm.snapshots.waitUntilReady(snapshotId)`. |\n| `snapshot-limit`, 429 | The API is running as many captures as it can. `capture` and `env.snapshot()` already retried it for 60 seconds. | Wait, then capture again. |\n| `SnapshotFailedError` | The capture failed. `err.snapshot.errorCode` is `capture-failed`, or `capture-interrupted` when the API restarted mid-capture. The snapshot is kept. | Capture again with a new idempotency key (the default); the old key returns the same failed snapshot. |\n| `SnapshotTimeoutError` | The snapshot wasn't `ready` within `readyTimeoutMs`. The capture continues on the server. | `await sm.snapshots.waitUntilReady(err.snapshotId)`. |\n| `forbidden`, 403, from `sm.snapshots.delete` | The key lacks `snapshots:delete`. Keys created by members never hold it. | Use a key whose creator holds `snapshots:delete`, or keep the snapshot. |\n| `release-unavailable`, 422, on a restore | No release on this deployment matches the snapshot's apps. | Start from `apps`, seed again and capture a new snapshot. |\n| `TypeError: Pass apiKey or set STATE_API_KEY.` | No key in the environment. | Ask the user to set `STATE_API_KEY`; in Workers, pass `apiKey`. |\n\nMore: https://state-web.usestatemachines.workers.dev/llms.txt and https://www.npmjs.com/package/@usestatemachines/sdk\n";
|
|
10
|
+
"---\nname: state-machines\ndescription: Use when an agent or test needs a disposable replica of a SaaS app such as Salesforce, to run integration tests or try API calls against a sandboxed copy instead of a real org. Covers the @usestatemachines/sdk TypeScript SDK - starting environments, choosing an actor, snapshots, suspending and resuming, cleanup, and errors such as environment-limit and release-unavailable. Also use when the user mentions State Machines, STATE_API_KEY or @usestatemachines/sdk.\n---\n\nRead `node_modules/@usestatemachines/sdk/README.md` before relying on memory; it matches the installed version. If the package is missing, run `npm install @usestatemachines/sdk`.\n\n## Workflow\n\n1. Check that `STATE_API_KEY` is set without printing it (`test -n \"$STATE_API_KEY\"`). If it isn't, ask the user to set it; the SDK doesn't read `.env` files.\n2. Start the environment with only the apps the task needs: `const env = await sm.environments.start({ apps: ['salesforce'] })`. Log `await sm.catalog.get()` for each app's ID and the actors and API profiles its release offers; don't guess IDs. A string app means `{ key: s, appId: s }`; use `{ key, appId }` for your own key. `start()` waits until the environment is `running`. If that fails, it stops the environment and rethrows; an `EnvironmentFailedError` or `TimeoutError` carries `err.environmentId` and `err.stopped`.\n3. Open a `try` right after `start()` returns, and call `env.stop()` in its `finally`.\n4. Connect through one API profile. Salesforce offers several and has no default, so pass `apiId`: `env.connect('salesforce', { apiId: 'rest' })`. This acts as the environment's declared default connection's actor, or else the app's first actor, the administrator. To act with another user's permissions, choose one from `await env.actors('salesforce')` and pass its `id` as `actorId`. Salesforce actor IDs are its user IDs, such as `005…`, never a role name, so read them instead of hard-coding one.\n5. Call the app with the connection's `fetch`. A leading `/` means the root of `baseUrl`.\n6. In `finally`, `await env.stop()`; it ends the environment and discards its data. In CI, then `await env.waitUntilStopped()`.\n\nThe whole flow, as the package ships it. It is ESM with top-level `await`: save it as `state-quickstart.mts` and run `npx tsx state-quickstart.mts`.\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATE_API_KEY\nconst env = await sm.environments.start({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce', { apiId: 'rest' });\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.stop();\n}\n```\n\n## Rules\n\n- Always stop in `finally`. `waitUntilStopped` confirms every app was disposed; use it in CI so a run fails when cleanup can't be confirmed.\n- `env.suspend()` ends the sandboxes and keeps the data, with the lifetime paused; it is not cleanup. Await `env.waitUntilSuspended()` before `env.resume()`, then `env.waitUntilReady()` before connecting again. A suspended environment still needs `env.stop()`.\n- To keep a failed environment for inspection, use the full-control path: `sm.environments.create`, then `sm.environments.waitUntilReady(environment.id)`, then `sm.environments.connect(environment.id, key, options)`. Nothing on that path stops the environment for you.\n- Lifetime counts from the start, including boot. It defaults to 15 minutes, or the deployment maximum if that is lower: `(await sm.credential.get()).limits.environmentLifetime`. Set `lifetimeMinutes` in `start` or `create` for longer runs.\n- Live environments are capped per organization: `limits.liveEnvironments` from `sm.credential.get()`. Stop the ones you no longer need; suspended ones don't count until they resume.\n- Never print, log or commit `STATE_API_KEY`, tokens, or `headers()` or `actorToken()` output.\n- Salesforce's SOAP profile (`soap-partner`) takes the actor token in the envelope, not a header: put `connection.actorToken()` in `SessionHeader/sessionId`. For it, `headers()` carries only `X-State-Environment-Token`.\n- `connection.fetch` never sends your API key, never follows redirects and only reaches URLs under `baseUrl`.\n- Runtimes: Node.js 20.20 or later (22 or 24 recommended), and Cloudflare Workers. ESM only. TypeScript needs `lib: [\"DOM\"]` or `@types/node`. Never ship an API key to a browser.\n- In Cloudflare Workers, pass the key explicitly: `new StateMachines({ apiKey: env.STATE_API_KEY })`, and bound calls with `{ signal: AbortSignal.timeout(20_000) }`, since retries can take minutes.\n- `environments.list`, `environments.events`, `workspaces.events` and `snapshots.list` return one page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`.\n- To reuse seeded data, `await env.snapshot({ name })` once, then `sm.environments.start({ snapshotId })` in place of `apps`. Capturing needs `snapshots:create` and `snapshots:read`. Snapshots are never deleted automatically, not even failed ones.\n- Check errors with `instanceof` (`StateMachinesError`, `EnvironmentFailedError`, `DisposalIncompleteError`, `TimeoutError`, `SnapshotFailedError`, `SnapshotTimeoutError`), not by `name`.\n\n## Recovering a failed create\n\nThe SDK already retries `502`, `503`, `504` and connection errors with one idempotency key. If `start()` or `create` still throws with `err.idempotencyKey`, the create failed: don't call it again as a new request, which can start a second environment. Call `sm.environments.start(input, { idempotencyKey: err.idempotencyKey })` with the same `input`. Don't replay when `err.status` is `null` and `err.cause` is a `TypeError`: the input was invalid and nothing was sent; fix the input instead. A key only recovers a failed create: don't reuse the key of a `start()` whose wait failed, because the replay returns that stopped environment. With your own key, `start()` doesn't stop the environment on failure, because another `start()` with that key may be using it: the error carries `err.environmentId`, `err.stopped` is `false` and there is no `err.stopError`. Stop it only once nothing else needs it. It returns the environment the first call created, or creates it if that call never arrived. A different input fails with `idempotency-conflict`. Keys are kept for a day.\n\n## Troubleshooting\n\n| Symptom | Cause | Fix |\n| --- | --- | --- |\n| `release-unavailable`, 422 | Unknown or retired `appId` or `appReleaseId`. A string app is used as the `appId`. | Check the IDs against `sm.catalog.get()`. Omit `appReleaseId` for the default release. |\n| `release-unavailable`, 503, or a failed app with that `errorCode` | This deployment doesn't serve the release. The SDK retries a 503 within a 10-minute budget. | Omit `appReleaseId`. If it persists, report the `requestId`. Pass a `signal` to bound the wait. |\n| `environment-limit`, 429 | The organization is at its live-environment cap. Not retried. | `sm.environments.list({ status: 'live' })`, stop or suspend one you no longer need, then start again. |\n| `not-suspended` or `suspend-in-progress`, 409 | `resume()` before the environment finished suspending, or `stop()` while it suspends. | Await `env.waitUntilSuspended()` first. |\n| `lifetime-too-long`, 422 | `lifetimeMinutes` is above the maximum. | Use at most `limits.environmentLifetime.maximumMinutes`. |\n| `TypeError: Unknown actor <id> for <appKey>. Choose one of: <ids>.` | The `actorId` isn't one of the app's actors. No token was revealed. | Pass an `id` from `await env.actors(appKey)`, or omit `actorId` to act as the default actor. |\n| `TypeError: Choose one API for <appKey>: <ids>.` | The app has several API profiles and `apiId` is missing or unknown. Salesforce always needs one. | Pass `apiId` from the listed IDs, such as `rest`. |\n| `start()` rejects with `EnvironmentFailedError` or `TimeoutError` | The environment failed, or wasn't `running` within `readyTimeoutMs` (10 minutes by default). If `err.stopped` is `true`, `start()` already stopped it. | Read `err.environment` (`null` on a `TimeoutError` that saw no read): its `stopReason` and each app's `errorCode`. If `err.stopped` is `false`, read `err.stopError` and call `sm.environments.stop(err.environmentId)`. To keep a failed environment, use the full-control path. |\n| `StateMachinesError` (`status` null) for invalid `apps`: `Pass at least one app.`, `App key \"<key>\" is used twice.` or `\"<s>\" can't be used as an app key; pass { key, appId } instead.` | Empty list, duplicate key, or a string that isn't a valid key. No request was sent. | Pass one to ten apps with unique keys; use `{ key, appId }` when the ID isn't a valid key. |\n| `TypeError: App ... is not ready to connect` | On the full-control path, connect() ran before the app was `ready`. | Await `waitUntilReady` first, or use `start()`. |\n| `connection-unavailable`, 409 | The app stopped, or the environment is no longer `running`. | Check `sm.environments.get(env.id)`; if it is suspended, resume it; if it stopped or expired, start a new one. |\n| `EnvironmentFailedError` from `waitUntilReady` | On the full-control path, the environment became `failed`, `suspending`, `suspended`, `stopping` or `stopped`. | Read `err.environment.status`, its `stopReason` and each app's `errorCode`. Still stop it in `finally`. |\n| A vendor client such as jsforce is rejected by the gateway | The gateway needs `X-State-Environment-Token` on every request; jsforce only sends it with custom headers configured. | Prefer `connection.fetch`. If you pass `headers()` to another client, send them only to `baseUrl` and disable redirects. |\n| `not-ready`, 409 | Capture: the environment isn't `running` or an app isn't `ready`. Restore: the snapshot isn't `ready` yet. Delete: the snapshot is still `capturing`. | Capture from the handle `start()` returns. Before a restore or delete, `await sm.snapshots.waitUntilReady(snapshotId)`. |\n| `capture-in-progress`, 409 | This environment is already capturing; one capture runs at a time. | `await sm.snapshots.waitUntilReady(<the running snapshot's id>)`, then capture again. |\n| `snapshot-limit`, 429 | Older APIs only: too many captures were running. `capture` and `env.snapshot()` already retried it for 60 seconds. | Wait, then capture again. |\n| `SnapshotFailedError` | The capture failed. `err.snapshot.errorCode` is `capture-failed`, or `capture-interrupted` when the API restarted mid-capture. The snapshot is kept. | Capture again with a new idempotency key (the default); the old key returns the same failed snapshot. |\n| `SnapshotTimeoutError` | The snapshot wasn't `ready` within `readyTimeoutMs`. The capture continues on the server. | `await sm.snapshots.waitUntilReady(err.snapshotId)`. |\n| `forbidden`, 403, from `sm.snapshots.delete` | The key lacks `snapshots:delete`. Keys created by members never hold it. | Use a key whose creator holds `snapshots:delete`, or keep the snapshot. |\n| `release-unavailable`, 422, on a restore | No release on this deployment matches the snapshot's apps. | Start from `apps`, seed again and capture a new snapshot. |\n| `TypeError: Pass apiKey or set STATE_API_KEY.` | No key in the environment. | Ask the user to set `STATE_API_KEY`; in Workers, pass `apiKey`. |\n\nMore: https://state-web.usestatemachines.workers.dev/llms.txt and https://www.npmjs.com/package/@usestatemachines/sdk\n";
|
|
11
11
|
|
|
12
12
|
export const quickstart =
|
|
13
13
|
"import { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATE_API_KEY\nconst env = await sm.environments.start({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce', { apiId: 'rest' });\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.stop();\n}\n";
|
|
14
14
|
|
|
15
15
|
export const templates = Object.freeze({
|
|
16
16
|
prompt:
|
|
17
|
-
"Set up State Machines so this project can run code against a disposable replica of a SaaS app, such as Salesforce. Follow these steps in order.\n\n1. Install the SDK: `npm install @usestatemachines/sdk`. It is ESM only and runs on Node.js 20.20 or later and on Cloudflare Workers, not in browsers.\n2. Check that `STATE_API_KEY` is set without printing it: `test -n \"$STATE_API_KEY\"`. If it is missing, stop and ask me to set it; the SDK doesn't read `.env` files. In Cloudflare Workers, pass the key as `new StateMachines({ apiKey })`.\n3. Save this quickstart unchanged as `state-quickstart.mts` and run `npx tsx state-quickstart.mts` to confirm the setup. It prints `200` and the org's limits:\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATE_API_KEY\nconst env = await sm.environments.start({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce', { apiId: 'rest' });\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.stop();\n}\n```\n\n4. Adapt it to my task. Log `await sm.catalog.get()` to find each app's ID and the actors and API profiles its release offers; don't guess IDs. Start only the apps the task needs; a string such as `'salesforce'` means `{ key: 'salesforce', appId: 'salesforce' }`. Salesforce always needs an `apiId`. `connect()` acts as the declared default connection's actor, or else the app's first actor, the administrator; to act as a specific user, pass an `actorId` from `await env.actors('salesforce')`. To reuse seeded data, `await env.snapshot()` once, then `sm.environments.start({ snapshotId })`.\n5. Always call `env.stop()` in a `finally
|
|
17
|
+
"Set up State Machines so this project can run code against a disposable replica of a SaaS app, such as Salesforce. Follow these steps in order.\n\n1. Install the SDK: `npm install @usestatemachines/sdk`. It is ESM only and runs on Node.js 20.20 or later and on Cloudflare Workers, not in browsers.\n2. Check that `STATE_API_KEY` is set without printing it: `test -n \"$STATE_API_KEY\"`. If it is missing, stop and ask me to set it; the SDK doesn't read `.env` files. In Cloudflare Workers, pass the key as `new StateMachines({ apiKey })`.\n3. Save this quickstart unchanged as `state-quickstart.mts` and run `npx tsx state-quickstart.mts` to confirm the setup. It prints `200` and the org's limits:\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATE_API_KEY\nconst env = await sm.environments.start({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce', { apiId: 'rest' });\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.stop();\n}\n```\n\n4. Adapt it to my task. Log `await sm.catalog.get()` to find each app's ID and the actors and API profiles its release offers; don't guess IDs. Start only the apps the task needs; a string such as `'salesforce'` means `{ key: 'salesforce', appId: 'salesforce' }`. Salesforce always needs an `apiId`. `connect()` acts as the declared default connection's actor, or else the app's first actor, the administrator; to act as a specific user, pass an `actorId` from `await env.actors('salesforce')`. To reuse seeded data, `await env.snapshot()` once, then `sm.environments.start({ snapshotId })`.\n5. Always call `env.stop()` in a `finally`; it ends the environment and discards its data. In CI, then await `env.waitUntilStopped()` so the run fails if cleanup isn't confirmed. If `start()` fails after creating the environment, it stops it; an `EnvironmentFailedError` or `TimeoutError` carries `err.environmentId` and `err.stopped`. If the create itself fails, call `sm.environments.start(input, { idempotencyKey: err.idempotencyKey })` with the same `input`; never retry without the key, which can start a second environment.\n6. Never print, log or commit keys or tokens, including `connection.headers()` and `connection.actorToken()`, which return live tokens.\n\nRead more: `node_modules/@usestatemachines/sdk/README.md`, {{origin}}/llms.txt and https://www.npmjs.com/package/@usestatemachines/sdk\n",
|
|
18
18
|
llmsTxt:
|
|
19
|
-
"# State Machines\n\n> State Machines runs disposable environments of replica SaaS apps, such as Salesforce, for tests and coding agents. Start, connect to and stop them with the TypeScript SDK `@usestatemachines/sdk`.\n\nInstall with `npm install @usestatemachines/sdk`. It is ESM only and runs on Node.js 20.20 or later (22 or 24 recommended) and Cloudflare Workers, not browsers. TypeScript needs `lib: [\"DOM\"]` or `@types/node`. `new StateMachines()` reads the API key from `STATE_API_KEY` and doesn't read `.env` files; in Cloudflare Workers, pass `apiKey` explicitly. Never print the key. The installed `node_modules/@usestatemachines/sdk/README.md` matches your version: read it before relying on memory.\n\n## Concepts\n\n- Environment: one disposable run of one or more apps. `const env = await sm.environments.start({ apps: ['salesforce'] })` creates it, waits until it is `running` and returns a `RunningEnvironment`. If the environment fails or the wait times out or is aborted, `start()` stops the environment and rethrows the original error; an `EnvironmentFailedError` or `TimeoutError` carries `err.environmentId` and `err.stopped`. Always call `env.stop()` in a `finally`.\n- Apps: a string is shorthand for `{ key: s, appId: s }`, nothing more. Use the object form, such as `{ key: 'crm', appId: 'salesforce' }`, for your own key or two instances of one app. `appId` names the app in the catalog (`sm.catalog.get()`); `key` is your name for it in this environment, and later calls take the key.\n- Actor: one of the app's users; requests run with that user's permissions. Salesforce actor IDs are its user IDs, such as `005…`, never a role name. `await env.actors('salesforce')` lists them in the release's order. connect() acts as the environment's declared default connection's actor, or else the first actor, the administrator, unless you pass `actorId`; pick a less-privileged actor to test permissions.\n- API profile: one API of the app, with its path and versions. Salesforce offers several (`rest`, `soap-partner`, `bulk2`, `oauth`) and has no default, so always pass `apiId`: `env.connect('salesforce', { apiId: 'rest' })`.\n- Connection: `connection.fetch` never sends your API key, never follows redirects and never retries. `connection.headers()` returns live tokens: treat them as secrets. Salesforce's SOAP profile (`soap-partner`) takes the actor token in the envelope instead: put `connection.actorToken()`, also a secret, in `SessionHeader/sessionId`; `headers()` then carries only `X-State-Environment-Token`.\n- Full control: `sm.environments.create(input)` returns one `Environment` before the apps boot, `sm.environments.waitUntilReady(environment.id)` waits, and `sm.environments.connect(environment.id, key, options)` connects. Nothing on this path stops the environment for you; use it to keep a failed environment for inspection.\n- Snapshot: the saved data of a running environment's apps, taken per app instance. `const snapshot = await env.snapshot({ name: 'seeded' })` captures it and waits until it is `ready`. `sm.environments.start({ snapshotId: snapshot.id })` starts a new environment from it, in place of `apps`; each app keeps its key. Capturing needs `snapshots:create` and `snapshots:read`, restoring needs `snapshots:read`, `rename` needs `snapshots:create`, and `sm.snapshots.delete(id)` needs `snapshots:delete`, which keys created by members don't hold. Snapshots are never deleted automatically, not even failed or timed-out ones: `SnapshotFailedError` and `SnapshotTimeoutError` carry `err.snapshotId`. `sm.snapshots` also has `create`, `waitUntilReady`, `get`, `list` and `rename`.\n- Vendor clients: the gateway needs `X-State-Environment-Token` on every request. A client such as jsforce only sends it if you configure custom headers. Prefer `connection.fetch`; if you pass `headers()` to another client, send them only to `connection.baseUrl` and disable redirects.\n- Pagination: `environments.list`, `environments.events`, `workspaces.events` and `snapshots.list` return one page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`.\n\n## Statuses\n\nEnums are open: handle values you don't recognise.\n\n- Environment `status`: `queued`, `starting`, `running`, `stopping`, `stopped`, `failed`. `stopReason`: `requested`, `expired`, `app_failed`, `owner_lost`.\n- App `status`: `pending`, `starting`, `ready`, `stopping`, `stopped`, `failed`. A failed app carries `errorCode`.\n- App `disposal`: `pending`, `complete`, `failed`, `unknown`.\n- Snapshot `status`: `capturing`, `ready`, `failed`. A failed snapshot carries `errorCode`: `capture-failed`, or `capture-interrupted` when the API restarted mid-capture.\n- `start()` and `waitUntilReady` throw `EnvironmentFailedError` if the environment becomes `failed`, `stopping` or `stopped`, and `TimeoutError` after 10 minutes by default (`readyTimeoutMs` for `start()`, `timeoutMs` for `waitUntilReady`). `start()` then stops the environment; `waitUntilReady` never does.\n- `waitUntilStopped` resolves when the environment is `stopped` or `failed` and every app's disposal is `complete`. It throws `DisposalIncompleteError` if a disposal is `failed` or `unknown`, and `TimeoutError` if disposal stays `pending`.\n\n## Limits\n\n- Live environments are capped per organization: `(await sm.credential.get()).limits.liveEnvironments`.\n- `lifetimeMinutes` defaults to 15 minutes, or the deployment maximum if that is lower. Both are in `(await sm.credential.get()).limits.environmentLifetime`. Lifetime counts from the start, so boot uses part of it; at the end the environment stops with `stopReason: 'expired'`.\n- Always call `env.stop()` in a `finally`. In CI, then await `env.waitUntilStopped()`.\n\n## Errors and retries\n\nA failed API call throws `StateMachinesError` with `status`, `code`, `requestId` and `retryAfter`. The SDK retries `502`, `503`, `504`, connection errors and attempt timeouts itself, within a 10-minute retry budget (the last attempt can end later); `create` retries reuse one idempotency key. It retries nothing else: not `429`, other `4xx` or other `5xx`. The one exception is snapshot capture, which retries `429` `snapshot-limit` for up to 60 seconds with one idempotency key. `err.stopped === false` with `err.stopError` means the stop wasn't confirmed; `sm.environments.stop(err.environmentId)` is safe to repeat. Without `stopError`, you passed your own `idempotencyKey`, so `start()` left the environment running because another `start()` with that key may be using it; stop it only once nothing else needs it. Every error from `create`, including the create step of `start()`, carries `err.idempotencyKey`: `sm.environments.start(input, { idempotencyKey: err.idempotencyKey })` with the same input returns the environment the first call created. Invalid `apps` fail before any request as a `StateMachinesError` (`status` null, `cause` a `TypeError`; don't replay it) with one of: `Pass at least one app.`, `App key \"<key>\" is used twice.` or `\"<s>\" can't be used as an app key; pass { key, appId } instead.`\n\n| Code | Status | What to do |\n| --- | --- | --- |\n| `environment-limit` | 429 | Not retried. Stop a live environment (`sm.environments.list({ status: 'live' })`), then start again. |\n| `lifetime-too-long` | 422 | Lower `lifetimeMinutes` to at most `limits.environmentLifetime.maximumMinutes`. |\n| `release-unavailable` | 422 | Unknown or retired app or release. Check `appId` and `appReleaseId` against `sm.catalog.get()`. |\n| `release-unavailable` | 503 | This deployment doesn't serve the release. Retried within the budget; omit `appReleaseId` to use the default release, or contact support with the `requestId`. A failed app can carry it as `errorCode`. |\n| `connection-unavailable` | 409 | The app stopped or the environment is no longer `running`. If it stopped or expired, start a new one. (Before revealing any token, connect() throws a `TypeError` for an app that isn't ready, an unknown actor, or a missing `apiId`.) |\n| `idempotency-conflict` | 409 | The idempotency key was used with a different input. Replay with the exact same input. |\n| `snapshot-limit` | 429 | Too many captures are running. `capture` and `env.snapshot()` retry it for up to 60 seconds; if it still fails, wait and capture again. |\n| `not-ready` | 409 | Capture: the environment isn't `running` or an app isn't `ready`. Restore: the snapshot isn't `ready` yet; `await sm.snapshots.waitUntilReady(id)`. Delete: the snapshot is still `capturing`. |\n| `version-conflict` | 409 | `rename` sent a stale `expectedVersion`, or the snapshot was deleted. `get` it: if `deletedAt` is set it's gone, otherwise retry with its `version`. |\n| `release-unavailable` | 422 | On a restore: no release on this deployment matches the snapshot's apps. Start from `apps`, seed again and capture a new snapshot. |\n| `request-timeout` | 503 | Retried automatically. If `create` still fails, replay it with `err.idempotencyKey`. |\n\n## Quickstart\n\nSave it unchanged as `state-quickstart.mts` and run `npx tsx state-quickstart.mts`.\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATE_API_KEY\nconst env = await sm.environments.start({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce', { apiId: 'rest' });\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.stop();\n}\n```\n\n## Docs\n\n- [npm package](https://www.npmjs.com/package/@usestatemachines/sdk): install, README and versions\n- [Agent skill]({{origin}}/skill.md): workflow, rules and troubleshooting for coding agents\n- [OpenAPI document](https://cdn.jsdelivr.net/npm/@usestatemachines/sdk/openapi.json): the HTTP API the SDK calls\n- [State Machines](https://usestatemachines.com): product site\n",
|
|
19
|
+
"# State Machines\n\n> State Machines runs disposable environments of replica SaaS apps, such as Salesforce, for tests and coding agents. Start, connect to, suspend, resume and stop them with the TypeScript SDK `@usestatemachines/sdk`.\n\nInstall with `npm install @usestatemachines/sdk`. It is ESM only and runs on Node.js 20.20 or later (22 or 24 recommended) and Cloudflare Workers, not browsers. TypeScript needs `lib: [\"DOM\"]` or `@types/node`. `new StateMachines()` reads the API key from `STATE_API_KEY` and doesn't read `.env` files; in Cloudflare Workers, pass `apiKey` explicitly. Never print the key. The installed `node_modules/@usestatemachines/sdk/README.md` matches your version: read it before relying on memory.\n\n## Concepts\n\n- Environment: one disposable run of one or more apps. `const env = await sm.environments.start({ apps: ['salesforce'] })` creates it, waits until it is `running` and returns a `RunningEnvironment`. If the environment fails or the wait times out or is aborted, `start()` stops the environment and rethrows the original error; an `EnvironmentFailedError` or `TimeoutError` carries `err.environmentId` and `err.stopped`. Always call `env.stop()` in a `finally`.\n- Suspend and resume: `env.suspend()` ends the sandboxes and keeps the data; the lifetime pauses. Await `env.waitUntilSuspended()`, later `env.resume()` and `env.waitUntilReady()` to run again on the same data. `env.stop()` ends the environment and discards its data; a suspended environment still needs it.\n- Apps: a string is shorthand for `{ key: s, appId: s }`, nothing more. Use the object form, such as `{ key: 'crm', appId: 'salesforce' }`, for your own key or two instances of one app. `appId` names the app in the catalog (`sm.catalog.get()`); `key` is your name for it in this environment, and later calls take the key.\n- Actor: one of the app's users; requests run with that user's permissions. Salesforce actor IDs are its user IDs, such as `005…`, never a role name. `await env.actors('salesforce')` lists them in the release's order. connect() acts as the environment's declared default connection's actor, or else the first actor, the administrator, unless you pass `actorId`; pick a less-privileged actor to test permissions.\n- API profile: one API of the app, with its path and versions. Salesforce offers several (`rest`, `soap-partner`, `bulk2`, `oauth`) and has no default, so always pass `apiId`: `env.connect('salesforce', { apiId: 'rest' })`.\n- Connection: `connection.fetch` never sends your API key, never follows redirects and never retries. `connection.headers()` returns live tokens: treat them as secrets. Salesforce's SOAP profile (`soap-partner`) takes the actor token in the envelope instead: put `connection.actorToken()`, also a secret, in `SessionHeader/sessionId`; `headers()` then carries only `X-State-Environment-Token`.\n- Full control: `sm.environments.create(input)` returns one `Environment` before the apps boot, `sm.environments.waitUntilReady(environment.id)` waits, and `sm.environments.connect(environment.id, key, options)` connects. Nothing on this path stops the environment for you; use it to keep a failed environment for inspection.\n- Snapshot: the saved data of a running environment's apps. Every app is paused while it is cut, so a multi-app snapshot is one instant (`consistency: 'environment'`); a single app is `'app_instance'`. `const snapshot = await env.snapshot({ name: 'seeded' })` captures it and waits until it is `ready`. `sm.environments.start({ snapshotId: snapshot.id })` starts a new environment from it, in place of `apps`; each app keeps its key. Capturing needs `snapshots:create` and `snapshots:read`, restoring needs `snapshots:read`, `rename` needs `snapshots:create`, and `sm.snapshots.delete(id)` needs `snapshots:delete`, which keys created by members don't hold. Snapshots are never deleted automatically, not even failed or timed-out ones: `SnapshotFailedError` and `SnapshotTimeoutError` carry `err.snapshotId`. `sm.snapshots` also has `create`, `waitUntilReady`, `get`, `list` and `rename`.\n- Recorded requests: `sm.environments.listRequests(env.id)` pages the calls made to the environment's apps (method, path, status, duration); `getRequest(env.id, id)` adds headers and bodies. Recording is best effort and SOAP bodies are not stored.\n- Vendor clients: the gateway needs `X-State-Environment-Token` on every request. A client such as jsforce only sends it if you configure custom headers. Prefer `connection.fetch`; if you pass `headers()` to another client, send them only to `connection.baseUrl` and disable redirects.\n- Pagination: `environments.list`, `environments.events`, `workspaces.events` and `snapshots.list` return one page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`.\n\n## Statuses\n\nEnums are open: handle values you don't recognise.\n\n- Environment `status`: `queued`, `starting`, `running`, `suspending`, `suspended`, `stopping`, `stopped`, `failed`. `stopReason`: `requested`, `expired`, `app_failed`, `owner_lost`. A suspended environment keeps its data and carries `suspendedAt`.\n- App `status`: `pending`, `starting`, `ready`, `recovering`, `suspended`, `stopping`, `stopped`, `failed`. A failed app carries `errorCode`.\n- App `disposal`: `pending`, `complete`, `failed`, `unknown`.\n- Snapshot `status`: `capturing`, `ready`, `failed`. A failed snapshot carries `errorCode`: `capture-failed`, `pause-failed` or `pause-lost` when the apps could not be held paused for the capture, or `capture-interrupted` when the API restarted mid-capture.\n- `start()` and `waitUntilReady` throw `EnvironmentFailedError` if the environment becomes `failed`, `suspending`, `suspended`, `stopping` or `stopped`, and `TimeoutError` after 10 minutes by default (`readyTimeoutMs` for `start()`, `timeoutMs` for `waitUntilReady`). `start()` then stops the environment; `waitUntilReady` never does.\n- `waitUntilSuspended` resolves when the environment is `suspended`, and throws `EnvironmentFailedError` if it ends instead.\n- `waitUntilStopped` resolves when the environment is `stopped` and every app's disposal is `complete`. A `failed` environment keeps its data for `resume()` and is not stopped until you call `stop()`. It throws `DisposalIncompleteError` if a disposal is `failed` or `unknown`, and `TimeoutError` if disposal stays `pending`.\n\n## Limits\n\n- Live environments are capped per organization: `(await sm.credential.get()).limits.liveEnvironments`.\n- `lifetimeMinutes` defaults to 15 minutes, or the deployment maximum if that is lower. Both are in `(await sm.credential.get()).limits.environmentLifetime`. Lifetime counts from the start, so boot uses part of it, and pauses while suspended; at the end the environment stops with `stopReason: 'expired'`.\n- A suspended environment doesn't count toward the live cap; resuming it does.\n- Always call `env.stop()` in a `finally`. In CI, then await `env.waitUntilStopped()`.\n\n## Errors and retries\n\nA failed API call throws `StateMachinesError` with `status`, `code`, `requestId` and `retryAfter`. The SDK retries `502`, `503`, `504`, connection errors and attempt timeouts itself, within a 10-minute retry budget (the last attempt can end later); `create` retries reuse one idempotency key. It retries nothing else: not `429`, other `4xx` or other `5xx`. The one exception is snapshot capture, which retries `429` `snapshot-limit` for up to 60 seconds with one idempotency key. `err.stopped === false` with `err.stopError` means the stop wasn't confirmed; `sm.environments.stop(err.environmentId)` is safe to repeat. Without `stopError`, you passed your own `idempotencyKey`, so `start()` left the environment running because another `start()` with that key may be using it; stop it only once nothing else needs it. Every error from `create`, including the create step of `start()`, carries `err.idempotencyKey`: `sm.environments.start(input, { idempotencyKey: err.idempotencyKey })` with the same input returns the environment the first call created. Invalid `apps` fail before any request as a `StateMachinesError` (`status` null, `cause` a `TypeError`; don't replay it) with one of: `Pass at least one app.`, `App key \"<key>\" is used twice.` or `\"<s>\" can't be used as an app key; pass { key, appId } instead.`\n\n| Code | Status | What to do |\n| --- | --- | --- |\n| `environment-limit` | 429 | Not retried. Suspend or stop a live environment (`sm.environments.list({ status: 'live' })`), then start or resume again. |\n| `not-ready` | 409 | Only a running environment whose apps are all ready can be suspended. Wait for it, or stop it. |\n| `not-suspended` | 409 | Only a suspended environment can resume. After `suspend()`, await `waitUntilSuspended()` first. |\n| `suspend-in-progress` | 409 | The environment is suspending. Await `waitUntilSuspended()`, then stop it. |\n| `lifetime-too-long` | 422 | Lower `lifetimeMinutes` to at most `limits.environmentLifetime.maximumMinutes`. |\n| `release-unavailable` | 422 | Unknown or retired app or release. Check `appId` and `appReleaseId` against `sm.catalog.get()`. |\n| `release-unavailable` | 503 | This deployment doesn't serve the release. Retried within the budget; omit `appReleaseId` to use the default release, or contact support with the `requestId`. A failed app can carry it as `errorCode`. |\n| `connection-unavailable` | 409 | The app stopped or the environment is no longer `running`. If it is suspended, resume it; if it stopped or expired, start a new one. (Before revealing any token, connect() throws a `TypeError` for an app that isn't ready, an unknown actor, or a missing `apiId`.) |\n| `idempotency-conflict` | 409 | The idempotency key was used with a different input. Replay with the exact same input. |\n| `capture-in-progress` | 409 | This environment is already capturing a snapshot; one runs at a time. Wait for it, then capture again. |\n| `snapshot-limit` | 429 | Older APIs only. `capture` and `env.snapshot()` retry it for up to 60 seconds; if it still fails, wait and capture again. |\n| `not-ready` | 409 | Capture: the environment isn't `running` or an app isn't `ready`. Restore: the snapshot isn't `ready` yet; `await sm.snapshots.waitUntilReady(id)`. Delete: the snapshot is still `capturing`. |\n| `version-conflict` | 409 | `rename` sent a stale `expectedVersion`, or the snapshot was deleted. `get` it: if `deletedAt` is set it's gone, otherwise retry with its `version`. |\n| `release-unavailable` | 422 | On a restore: no release on this deployment matches the snapshot's apps. Start from `apps`, seed again and capture a new snapshot. |\n| `request-timeout` | 503 | Retried automatically. If `create` still fails, replay it with `err.idempotencyKey`. |\n\n## Quickstart\n\nSave it unchanged as `state-quickstart.mts` and run `npx tsx state-quickstart.mts`.\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATE_API_KEY\nconst env = await sm.environments.start({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce', { apiId: 'rest' });\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.stop();\n}\n```\n\n## Docs\n\n- [npm package](https://www.npmjs.com/package/@usestatemachines/sdk): install, README and versions\n- [Agent skill]({{origin}}/skill.md): workflow, rules and troubleshooting for coding agents\n- [OpenAPI document](https://cdn.jsdelivr.net/npm/@usestatemachines/sdk/openapi.json): the HTTP API the SDK calls\n- [State Machines](https://usestatemachines.com): product site\n",
|
|
20
20
|
skill:
|
|
21
|
-
"---\nname: state-machines\ndescription: Use when an agent or test needs a disposable replica of a SaaS app such as Salesforce, to run integration tests or try API calls against a sandboxed copy instead of a real org. Covers the @usestatemachines/sdk TypeScript SDK - starting environments, choosing an actor, snapshots, cleanup, and errors such as environment-limit and release-unavailable. Also use when the user mentions State Machines, STATE_API_KEY or @usestatemachines/sdk.\n---\n\nRead `node_modules/@usestatemachines/sdk/README.md` before relying on memory; it matches the installed version. If the package is missing, run `npm install @usestatemachines/sdk`.\n\n## Workflow\n\n1. Check that `STATE_API_KEY` is set without printing it (`test -n \"$STATE_API_KEY\"`). If it isn't, ask the user to set it; the SDK doesn't read `.env` files.\n2. Start the environment with only the apps the task needs: `const env = await sm.environments.start({ apps: ['salesforce'] })`. Log `await sm.catalog.get()` for each app's ID and the actors and API profiles its release offers; don't guess IDs. A string app means `{ key: s, appId: s }`; use `{ key, appId }` for your own key. `start()` waits until the environment is `running`. If that fails, it stops the environment and rethrows; an `EnvironmentFailedError` or `TimeoutError` carries `err.environmentId` and `err.stopped`.\n3. Open a `try` right after `start()` returns, and call `env.stop()` in its `finally`.\n4. Connect through one API profile. Salesforce offers several and has no default, so pass `apiId`: `env.connect('salesforce', { apiId: 'rest' })`. This acts as the environment's declared default connection's actor, or else the app's first actor, the administrator. To act with another user's permissions, choose one from `await env.actors('salesforce')` and pass its `id` as `actorId`. Salesforce actor IDs are its user IDs, such as `005…`, never a role name, so read them instead of hard-coding one.\n5. Call the app with the connection's `fetch`. A leading `/` means the root of `baseUrl`.\n6. In `finally`, `await env.stop()`. In CI, then `await env.waitUntilStopped()`.\n\nThe whole flow, as the package ships it. It is ESM with top-level `await`: save it as `state-quickstart.mts` and run `npx tsx state-quickstart.mts`.\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATE_API_KEY\nconst env = await sm.environments.start({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce', { apiId: 'rest' });\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.stop();\n}\n```\n\n## Rules\n\n- Always stop in `finally`. `waitUntilStopped` confirms every app was disposed; use it in CI so a run fails when cleanup can't be confirmed.\n- To keep a failed environment for inspection, use the full-control path: `sm.environments.create`, then `sm.environments.waitUntilReady(environment.id)`, then `sm.environments.connect(environment.id, key, options)`. Nothing on that path stops the environment for you.\n- Lifetime counts from the start, including boot. It defaults to 15 minutes, or the deployment maximum if that is lower: `(await sm.credential.get()).limits.environmentLifetime`. Set `lifetimeMinutes` in `start` or `create` for longer runs.\n- Live environments are capped per organization: `limits.liveEnvironments` from `sm.credential.get()`. Stop the ones you no longer need.\n- Never print, log or commit `STATE_API_KEY`, tokens, or `headers()` or `actorToken()` output.\n- Salesforce's SOAP profile (`soap-partner`) takes the actor token in the envelope, not a header: put `connection.actorToken()` in `SessionHeader/sessionId`. For it, `headers()` carries only `X-State-Environment-Token`.\n- `connection.fetch` never sends your API key, never follows redirects and only reaches URLs under `baseUrl`.\n- Runtimes: Node.js 20.20 or later (22 or 24 recommended), and Cloudflare Workers. ESM only. TypeScript needs `lib: [\"DOM\"]` or `@types/node`. Never ship an API key to a browser.\n- In Cloudflare Workers, pass the key explicitly: `new StateMachines({ apiKey: env.STATE_API_KEY })`, and bound calls with `{ signal: AbortSignal.timeout(20_000) }`, since retries can take minutes.\n- `environments.list`, `environments.events`, `workspaces.events` and `snapshots.list` return one page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`.\n- To reuse seeded data, `await env.snapshot({ name })` once, then `sm.environments.start({ snapshotId })` in place of `apps`. Capturing needs `snapshots:create` and `snapshots:read`. Snapshots are never deleted automatically, not even failed ones.\n- Check errors with `instanceof` (`StateMachinesError`, `EnvironmentFailedError`, `DisposalIncompleteError`, `TimeoutError`, `SnapshotFailedError`, `SnapshotTimeoutError`), not by `name`.\n\n## Recovering a failed create\n\nThe SDK already retries `502`, `503`, `504` and connection errors with one idempotency key. If `start()` or `create` still throws with `err.idempotencyKey`, the create failed: don't call it again as a new request, which can start a second environment. Call `sm.environments.start(input, { idempotencyKey: err.idempotencyKey })` with the same `input`. Don't replay when `err.status` is `null` and `err.cause` is a `TypeError`: the input was invalid and nothing was sent; fix the input instead. A key only recovers a failed create: don't reuse the key of a `start()` whose wait failed, because the replay returns that stopped environment. With your own key, `start()` doesn't stop the environment on failure, because another `start()` with that key may be using it: the error carries `err.environmentId`, `err.stopped` is `false` and there is no `err.stopError`. Stop it only once nothing else needs it. It returns the environment the first call created, or creates it if that call never arrived. A different input fails with `idempotency-conflict`. Keys are kept for a day.\n\n## Troubleshooting\n\n| Symptom | Cause | Fix |\n| --- | --- | --- |\n| `release-unavailable`, 422 | Unknown or retired `appId` or `appReleaseId`. A string app is used as the `appId`. | Check the IDs against `sm.catalog.get()`. Omit `appReleaseId` for the default release. |\n| `release-unavailable`, 503, or a failed app with that `errorCode` | This deployment doesn't serve the release. The SDK retries a 503 within a 10-minute budget. | Omit `appReleaseId`. If it persists, report the `requestId`. Pass a `signal` to bound the wait. |\n| `environment-limit`, 429 | The organization is at its live-environment cap. Not retried. | `sm.environments.list({ status: 'live' })`, stop one you no longer need, then start again. |\n| `lifetime-too-long`, 422 | `lifetimeMinutes` is above the maximum. | Use at most `limits.environmentLifetime.maximumMinutes`. |\n| `TypeError: Unknown actor <id> for <appKey>. Choose one of: <ids>.` | The `actorId` isn't one of the app's actors. No token was revealed. | Pass an `id` from `await env.actors(appKey)`, or omit `actorId` to act as the default actor. |\n| `TypeError: Choose one API for <appKey>: <ids>.` | The app has several API profiles and `apiId` is missing or unknown. Salesforce always needs one. | Pass `apiId` from the listed IDs, such as `rest`. |\n| `start()` rejects with `EnvironmentFailedError` or `TimeoutError` | The environment failed, or wasn't `running` within `readyTimeoutMs` (10 minutes by default). If `err.stopped` is `true`, `start()` already stopped it. | Read `err.environment` (`null` on a `TimeoutError` that saw no read): its `stopReason` and each app's `errorCode`. If `err.stopped` is `false`, read `err.stopError` and call `sm.environments.stop(err.environmentId)`. To keep a failed environment, use the full-control path. |\n| `StateMachinesError` (`status` null) for invalid `apps`: `Pass at least one app.`, `App key \"<key>\" is used twice.` or `\"<s>\" can't be used as an app key; pass { key, appId } instead.` | Empty list, duplicate key, or a string that isn't a valid key. No request was sent. | Pass one to ten apps with unique keys; use `{ key, appId }` when the ID isn't a valid key. |\n| `TypeError: App ... is not ready to connect` | On the full-control path, connect() ran before the app was `ready`. | Await `waitUntilReady` first, or use `start()`. |\n| `connection-unavailable`, 409 | The app stopped, or the environment is no longer `running`. | Check `sm.environments.get(env.id)`; if it stopped or expired, start a new one. |\n| `EnvironmentFailedError` from `waitUntilReady` | On the full-control path, the environment became `failed`, `stopping` or `stopped`. | Read `err.environment.stopReason` and each app's `errorCode`. Still stop it in `finally`. |\n| A vendor client such as jsforce is rejected by the gateway | The gateway needs `X-State-Environment-Token` on every request; jsforce only sends it with custom headers configured. | Prefer `connection.fetch`. If you pass `headers()` to another client, send them only to `baseUrl` and disable redirects. |\n| `not-ready`, 409 | Capture: the environment isn't `running` or an app isn't `ready`. Restore: the snapshot isn't `ready` yet. Delete: the snapshot is still `capturing`. | Capture from the handle `start()` returns. Before a restore or delete, `await sm.snapshots.waitUntilReady(snapshotId)`. |\n| `snapshot-limit`, 429 | The API is running as many captures as it can. `capture` and `env.snapshot()` already retried it for 60 seconds. | Wait, then capture again. |\n| `SnapshotFailedError` | The capture failed. `err.snapshot.errorCode` is `capture-failed`, or `capture-interrupted` when the API restarted mid-capture. The snapshot is kept. | Capture again with a new idempotency key (the default); the old key returns the same failed snapshot. |\n| `SnapshotTimeoutError` | The snapshot wasn't `ready` within `readyTimeoutMs`. The capture continues on the server. | `await sm.snapshots.waitUntilReady(err.snapshotId)`. |\n| `forbidden`, 403, from `sm.snapshots.delete` | The key lacks `snapshots:delete`. Keys created by members never hold it. | Use a key whose creator holds `snapshots:delete`, or keep the snapshot. |\n| `release-unavailable`, 422, on a restore | No release on this deployment matches the snapshot's apps. | Start from `apps`, seed again and capture a new snapshot. |\n| `TypeError: Pass apiKey or set STATE_API_KEY.` | No key in the environment. | Ask the user to set `STATE_API_KEY`; in Workers, pass `apiKey`. |\n\nMore: {{origin}}/llms.txt and https://www.npmjs.com/package/@usestatemachines/sdk\n",
|
|
21
|
+
"---\nname: state-machines\ndescription: Use when an agent or test needs a disposable replica of a SaaS app such as Salesforce, to run integration tests or try API calls against a sandboxed copy instead of a real org. Covers the @usestatemachines/sdk TypeScript SDK - starting environments, choosing an actor, snapshots, suspending and resuming, cleanup, and errors such as environment-limit and release-unavailable. Also use when the user mentions State Machines, STATE_API_KEY or @usestatemachines/sdk.\n---\n\nRead `node_modules/@usestatemachines/sdk/README.md` before relying on memory; it matches the installed version. If the package is missing, run `npm install @usestatemachines/sdk`.\n\n## Workflow\n\n1. Check that `STATE_API_KEY` is set without printing it (`test -n \"$STATE_API_KEY\"`). If it isn't, ask the user to set it; the SDK doesn't read `.env` files.\n2. Start the environment with only the apps the task needs: `const env = await sm.environments.start({ apps: ['salesforce'] })`. Log `await sm.catalog.get()` for each app's ID and the actors and API profiles its release offers; don't guess IDs. A string app means `{ key: s, appId: s }`; use `{ key, appId }` for your own key. `start()` waits until the environment is `running`. If that fails, it stops the environment and rethrows; an `EnvironmentFailedError` or `TimeoutError` carries `err.environmentId` and `err.stopped`.\n3. Open a `try` right after `start()` returns, and call `env.stop()` in its `finally`.\n4. Connect through one API profile. Salesforce offers several and has no default, so pass `apiId`: `env.connect('salesforce', { apiId: 'rest' })`. This acts as the environment's declared default connection's actor, or else the app's first actor, the administrator. To act with another user's permissions, choose one from `await env.actors('salesforce')` and pass its `id` as `actorId`. Salesforce actor IDs are its user IDs, such as `005…`, never a role name, so read them instead of hard-coding one.\n5. Call the app with the connection's `fetch`. A leading `/` means the root of `baseUrl`.\n6. In `finally`, `await env.stop()`; it ends the environment and discards its data. In CI, then `await env.waitUntilStopped()`.\n\nThe whole flow, as the package ships it. It is ESM with top-level `await`: save it as `state-quickstart.mts` and run `npx tsx state-quickstart.mts`.\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATE_API_KEY\nconst env = await sm.environments.start({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce', { apiId: 'rest' });\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.stop();\n}\n```\n\n## Rules\n\n- Always stop in `finally`. `waitUntilStopped` confirms every app was disposed; use it in CI so a run fails when cleanup can't be confirmed.\n- `env.suspend()` ends the sandboxes and keeps the data, with the lifetime paused; it is not cleanup. Await `env.waitUntilSuspended()` before `env.resume()`, then `env.waitUntilReady()` before connecting again. A suspended environment still needs `env.stop()`.\n- To keep a failed environment for inspection, use the full-control path: `sm.environments.create`, then `sm.environments.waitUntilReady(environment.id)`, then `sm.environments.connect(environment.id, key, options)`. Nothing on that path stops the environment for you.\n- Lifetime counts from the start, including boot. It defaults to 15 minutes, or the deployment maximum if that is lower: `(await sm.credential.get()).limits.environmentLifetime`. Set `lifetimeMinutes` in `start` or `create` for longer runs.\n- Live environments are capped per organization: `limits.liveEnvironments` from `sm.credential.get()`. Stop the ones you no longer need; suspended ones don't count until they resume.\n- Never print, log or commit `STATE_API_KEY`, tokens, or `headers()` or `actorToken()` output.\n- Salesforce's SOAP profile (`soap-partner`) takes the actor token in the envelope, not a header: put `connection.actorToken()` in `SessionHeader/sessionId`. For it, `headers()` carries only `X-State-Environment-Token`.\n- `connection.fetch` never sends your API key, never follows redirects and only reaches URLs under `baseUrl`.\n- Runtimes: Node.js 20.20 or later (22 or 24 recommended), and Cloudflare Workers. ESM only. TypeScript needs `lib: [\"DOM\"]` or `@types/node`. Never ship an API key to a browser.\n- In Cloudflare Workers, pass the key explicitly: `new StateMachines({ apiKey: env.STATE_API_KEY })`, and bound calls with `{ signal: AbortSignal.timeout(20_000) }`, since retries can take minutes.\n- `environments.list`, `environments.events`, `workspaces.events` and `snapshots.list` return one page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`.\n- To reuse seeded data, `await env.snapshot({ name })` once, then `sm.environments.start({ snapshotId })` in place of `apps`. Capturing needs `snapshots:create` and `snapshots:read`. Snapshots are never deleted automatically, not even failed ones.\n- Check errors with `instanceof` (`StateMachinesError`, `EnvironmentFailedError`, `DisposalIncompleteError`, `TimeoutError`, `SnapshotFailedError`, `SnapshotTimeoutError`), not by `name`.\n\n## Recovering a failed create\n\nThe SDK already retries `502`, `503`, `504` and connection errors with one idempotency key. If `start()` or `create` still throws with `err.idempotencyKey`, the create failed: don't call it again as a new request, which can start a second environment. Call `sm.environments.start(input, { idempotencyKey: err.idempotencyKey })` with the same `input`. Don't replay when `err.status` is `null` and `err.cause` is a `TypeError`: the input was invalid and nothing was sent; fix the input instead. A key only recovers a failed create: don't reuse the key of a `start()` whose wait failed, because the replay returns that stopped environment. With your own key, `start()` doesn't stop the environment on failure, because another `start()` with that key may be using it: the error carries `err.environmentId`, `err.stopped` is `false` and there is no `err.stopError`. Stop it only once nothing else needs it. It returns the environment the first call created, or creates it if that call never arrived. A different input fails with `idempotency-conflict`. Keys are kept for a day.\n\n## Troubleshooting\n\n| Symptom | Cause | Fix |\n| --- | --- | --- |\n| `release-unavailable`, 422 | Unknown or retired `appId` or `appReleaseId`. A string app is used as the `appId`. | Check the IDs against `sm.catalog.get()`. Omit `appReleaseId` for the default release. |\n| `release-unavailable`, 503, or a failed app with that `errorCode` | This deployment doesn't serve the release. The SDK retries a 503 within a 10-minute budget. | Omit `appReleaseId`. If it persists, report the `requestId`. Pass a `signal` to bound the wait. |\n| `environment-limit`, 429 | The organization is at its live-environment cap. Not retried. | `sm.environments.list({ status: 'live' })`, stop or suspend one you no longer need, then start again. |\n| `not-suspended` or `suspend-in-progress`, 409 | `resume()` before the environment finished suspending, or `stop()` while it suspends. | Await `env.waitUntilSuspended()` first. |\n| `lifetime-too-long`, 422 | `lifetimeMinutes` is above the maximum. | Use at most `limits.environmentLifetime.maximumMinutes`. |\n| `TypeError: Unknown actor <id> for <appKey>. Choose one of: <ids>.` | The `actorId` isn't one of the app's actors. No token was revealed. | Pass an `id` from `await env.actors(appKey)`, or omit `actorId` to act as the default actor. |\n| `TypeError: Choose one API for <appKey>: <ids>.` | The app has several API profiles and `apiId` is missing or unknown. Salesforce always needs one. | Pass `apiId` from the listed IDs, such as `rest`. |\n| `start()` rejects with `EnvironmentFailedError` or `TimeoutError` | The environment failed, or wasn't `running` within `readyTimeoutMs` (10 minutes by default). If `err.stopped` is `true`, `start()` already stopped it. | Read `err.environment` (`null` on a `TimeoutError` that saw no read): its `stopReason` and each app's `errorCode`. If `err.stopped` is `false`, read `err.stopError` and call `sm.environments.stop(err.environmentId)`. To keep a failed environment, use the full-control path. |\n| `StateMachinesError` (`status` null) for invalid `apps`: `Pass at least one app.`, `App key \"<key>\" is used twice.` or `\"<s>\" can't be used as an app key; pass { key, appId } instead.` | Empty list, duplicate key, or a string that isn't a valid key. No request was sent. | Pass one to ten apps with unique keys; use `{ key, appId }` when the ID isn't a valid key. |\n| `TypeError: App ... is not ready to connect` | On the full-control path, connect() ran before the app was `ready`. | Await `waitUntilReady` first, or use `start()`. |\n| `connection-unavailable`, 409 | The app stopped, or the environment is no longer `running`. | Check `sm.environments.get(env.id)`; if it is suspended, resume it; if it stopped or expired, start a new one. |\n| `EnvironmentFailedError` from `waitUntilReady` | On the full-control path, the environment became `failed`, `suspending`, `suspended`, `stopping` or `stopped`. | Read `err.environment.status`, its `stopReason` and each app's `errorCode`. Still stop it in `finally`. |\n| A vendor client such as jsforce is rejected by the gateway | The gateway needs `X-State-Environment-Token` on every request; jsforce only sends it with custom headers configured. | Prefer `connection.fetch`. If you pass `headers()` to another client, send them only to `baseUrl` and disable redirects. |\n| `not-ready`, 409 | Capture: the environment isn't `running` or an app isn't `ready`. Restore: the snapshot isn't `ready` yet. Delete: the snapshot is still `capturing`. | Capture from the handle `start()` returns. Before a restore or delete, `await sm.snapshots.waitUntilReady(snapshotId)`. |\n| `capture-in-progress`, 409 | This environment is already capturing; one capture runs at a time. | `await sm.snapshots.waitUntilReady(<the running snapshot's id>)`, then capture again. |\n| `snapshot-limit`, 429 | Older APIs only: too many captures were running. `capture` and `env.snapshot()` already retried it for 60 seconds. | Wait, then capture again. |\n| `SnapshotFailedError` | The capture failed. `err.snapshot.errorCode` is `capture-failed`, or `capture-interrupted` when the API restarted mid-capture. The snapshot is kept. | Capture again with a new idempotency key (the default); the old key returns the same failed snapshot. |\n| `SnapshotTimeoutError` | The snapshot wasn't `ready` within `readyTimeoutMs`. The capture continues on the server. | `await sm.snapshots.waitUntilReady(err.snapshotId)`. |\n| `forbidden`, 403, from `sm.snapshots.delete` | The key lacks `snapshots:delete`. Keys created by members never hold it. | Use a key whose creator holds `snapshots:delete`, or keep the snapshot. |\n| `release-unavailable`, 422, on a restore | No release on this deployment matches the snapshot's apps. | Start from `apps`, seed again and capture a new snapshot. |\n| `TypeError: Pass apiKey or set STATE_API_KEY.` | No key in the environment. | Ask the user to set `STATE_API_KEY`; in Workers, pass `apiKey`. |\n\nMore: {{origin}}/llms.txt and https://www.npmjs.com/package/@usestatemachines/sdk\n",
|
|
22
22
|
});
|