@usestatemachines/sdk 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +311 -0
- package/agents/SKILL.md +90 -0
- package/agents/index.d.ts +13 -0
- package/agents/index.js +22 -0
- package/agents/llms.txt +93 -0
- package/agents/prompt.md +49 -0
- package/esm/core.d.ts +9 -0
- package/esm/core.js +12 -0
- package/esm/extensions/client.d.ts +81 -0
- package/esm/extensions/client.js +213 -0
- package/esm/extensions/connection.d.ts +42 -0
- package/esm/extensions/connection.js +133 -0
- package/esm/extensions/errors.d.ts +52 -0
- package/esm/extensions/errors.js +129 -0
- package/esm/extensions/index.d.ts +11 -0
- package/esm/extensions/index.js +7 -0
- package/esm/extensions/signal.d.ts +14 -0
- package/esm/extensions/signal.js +29 -0
- package/esm/extensions/wait.d.ts +19 -0
- package/esm/extensions/wait.js +102 -0
- package/esm/funcs/catalog-get.d.ts +11 -0
- package/esm/funcs/catalog-get.js +76 -0
- package/esm/funcs/catalog-release.d.ts +11 -0
- package/esm/funcs/catalog-release.js +92 -0
- package/esm/funcs/credential-get.d.ts +11 -0
- package/esm/funcs/credential-get.js +76 -0
- package/esm/funcs/environments-create.d.ts +11 -0
- package/esm/funcs/environments-create.js +94 -0
- package/esm/funcs/environments-events.d.ts +12 -0
- package/esm/funcs/environments-events.js +98 -0
- package/esm/funcs/environments-get-connection.d.ts +11 -0
- package/esm/funcs/environments-get-connection.js +96 -0
- package/esm/funcs/environments-get.d.ts +11 -0
- package/esm/funcs/environments-get.js +92 -0
- package/esm/funcs/environments-list.d.ts +11 -0
- package/esm/funcs/environments-list.js +100 -0
- package/esm/funcs/environments-reveal.d.ts +11 -0
- package/esm/funcs/environments-reveal.js +97 -0
- package/esm/funcs/environments-stop.d.ts +11 -0
- package/esm/funcs/environments-stop.js +92 -0
- package/esm/funcs/workspaces-events.d.ts +12 -0
- package/esm/funcs/workspaces-events.js +98 -0
- package/esm/funcs/workspaces-get.d.ts +11 -0
- package/esm/funcs/workspaces-get.js +92 -0
- package/esm/funcs/workspaces-list.d.ts +11 -0
- package/esm/funcs/workspaces-list.js +90 -0
- package/esm/hooks/hooks.d.ts +24 -0
- package/esm/hooks/hooks.js +79 -0
- package/esm/hooks/index.d.ts +2 -0
- package/esm/hooks/index.js +5 -0
- package/esm/hooks/types.d.ts +76 -0
- package/esm/hooks/types.js +4 -0
- package/esm/index.d.ts +5 -0
- package/esm/index.js +7 -0
- package/esm/lib/base64.d.ts +9 -0
- package/esm/lib/base64.js +30 -0
- package/esm/lib/config.d.ts +37 -0
- package/esm/lib/config.js +30 -0
- package/esm/lib/encodings.d.ts +54 -0
- package/esm/lib/encodings.js +406 -0
- package/esm/lib/files.d.ts +25 -0
- package/esm/lib/files.js +90 -0
- package/esm/lib/http.d.ts +66 -0
- package/esm/lib/http.js +208 -0
- package/esm/lib/logger.d.ts +5 -0
- package/esm/lib/logger.js +4 -0
- package/esm/lib/matchers.d.ts +58 -0
- package/esm/lib/matchers.js +201 -0
- package/esm/lib/primitives.d.ts +26 -0
- package/esm/lib/primitives.js +119 -0
- package/esm/lib/retries.d.ts +37 -0
- package/esm/lib/retries.js +156 -0
- package/esm/lib/schemas.d.ts +20 -0
- package/esm/lib/schemas.js +56 -0
- package/esm/lib/sdks.d.ts +62 -0
- package/esm/lib/sdks.js +289 -0
- package/esm/lib/security.d.ts +83 -0
- package/esm/lib/security.js +145 -0
- package/esm/lib/url.d.ts +4 -0
- package/esm/lib/url.js +23 -0
- package/esm/models/actor.d.ts +12 -0
- package/esm/models/actor.js +16 -0
- package/esm/models/api-profile.d.ts +17 -0
- package/esm/models/api-profile.js +22 -0
- package/esm/models/app.d.ts +14 -0
- package/esm/models/app.js +18 -0
- package/esm/models/catalog.d.ts +12 -0
- package/esm/models/catalog.js +15 -0
- package/esm/models/connection-secret.d.ts +14 -0
- package/esm/models/connection-secret.js +18 -0
- package/esm/models/connection.d.ts +26 -0
- package/esm/models/connection.js +30 -0
- package/esm/models/create-environment-input.d.ts +35 -0
- package/esm/models/create-environment-input.js +23 -0
- package/esm/models/credential-kind.d.ts +9 -0
- package/esm/models/credential-kind.js +10 -0
- package/esm/models/credential.d.ts +48 -0
- package/esm/models/credential.js +44 -0
- package/esm/models/environment-app.d.ts +42 -0
- package/esm/models/environment-app.js +44 -0
- package/esm/models/environment-lifetime.d.ts +10 -0
- package/esm/models/environment-lifetime.js +14 -0
- package/esm/models/environment.d.ts +47 -0
- package/esm/models/environment.js +48 -0
- package/esm/models/error-response.d.ts +45 -0
- package/esm/models/error-response.js +47 -0
- package/esm/models/errors/error-response.d.ts +18 -0
- package/esm/models/errors/error-response.js +32 -0
- package/esm/models/errors/http-client-errors.d.ts +43 -0
- package/esm/models/errors/http-client-errors.js +55 -0
- package/esm/models/errors/index.d.ts +6 -0
- package/esm/models/errors/index.js +9 -0
- package/esm/models/errors/response-validation-error.d.ts +25 -0
- package/esm/models/errors/response-validation-error.js +36 -0
- package/esm/models/errors/sdk-validation-error.d.ts +20 -0
- package/esm/models/errors/sdk-validation-error.js +51 -0
- package/esm/models/errors/state-machines-http-error.d.ts +18 -0
- package/esm/models/errors/state-machines-http-error.js +25 -0
- package/esm/models/errors/unexpected-response-error.d.ts +9 -0
- package/esm/models/errors/unexpected-response-error.js +29 -0
- package/esm/models/event-page.d.ts +11 -0
- package/esm/models/event-page.js +16 -0
- package/esm/models/event.d.ts +20 -0
- package/esm/models/event.js +22 -0
- package/esm/models/index.d.ts +19 -0
- package/esm/models/index.js +22 -0
- package/esm/models/operations/create-environments.d.ts +27 -0
- package/esm/models/operations/create-environments.js +27 -0
- package/esm/models/operations/get-catalog.d.ts +13 -0
- package/esm/models/operations/get-catalog.js +13 -0
- package/esm/models/operations/get-connection.d.ts +25 -0
- package/esm/models/operations/get-connection.js +21 -0
- package/esm/models/operations/get-credential.d.ts +13 -0
- package/esm/models/operations/get-credential.js +13 -0
- package/esm/models/operations/get-environment.d.ts +23 -0
- package/esm/models/operations/get-environment.js +20 -0
- package/esm/models/operations/get-release.d.ts +23 -0
- package/esm/models/operations/get-release.js +20 -0
- package/esm/models/operations/get-workspace.d.ts +23 -0
- package/esm/models/operations/get-workspace.js +20 -0
- package/esm/models/operations/index.d.ts +13 -0
- package/esm/models/operations/index.js +16 -0
- package/esm/models/operations/list-environment-events.d.ts +15 -0
- package/esm/models/operations/list-environment-events.js +13 -0
- package/esm/models/operations/list-environments.d.ts +49 -0
- package/esm/models/operations/list-environments.js +40 -0
- package/esm/models/operations/list-workspace-events.d.ts +15 -0
- package/esm/models/operations/list-workspace-events.js +13 -0
- package/esm/models/operations/list-workspaces.d.ts +32 -0
- package/esm/models/operations/list-workspaces.js +27 -0
- package/esm/models/operations/reveal-connection.d.ts +37 -0
- package/esm/models/operations/reveal-connection.js +29 -0
- package/esm/models/operations/stop-environment.d.ts +23 -0
- package/esm/models/operations/stop-environment.js +20 -0
- package/esm/models/release-actor.d.ts +11 -0
- package/esm/models/release-actor.js +15 -0
- package/esm/models/release.d.ts +26 -0
- package/esm/models/release.js +29 -0
- package/esm/models/security.d.ts +11 -0
- package/esm/models/security.js +11 -0
- package/esm/models/workspace.d.ts +16 -0
- package/esm/models/workspace.js +21 -0
- package/esm/sdk/catalog.d.ts +6 -0
- package/esm/sdk/catalog.js +15 -0
- package/esm/sdk/credential.d.ts +5 -0
- package/esm/sdk/credential.js +11 -0
- package/esm/sdk/environments.d.ts +12 -0
- package/esm/sdk/environments.js +35 -0
- package/esm/sdk/index.d.ts +1 -0
- package/esm/sdk/index.js +4 -0
- package/esm/sdk/sdk.d.ts +15 -0
- package/esm/sdk/sdk.js +26 -0
- package/esm/sdk/workspaces.d.ts +8 -0
- package/esm/sdk/workspaces.js +19 -0
- package/esm/types/async.d.ts +22 -0
- package/esm/types/async.js +27 -0
- package/esm/types/blobs.d.ts +3 -0
- package/esm/types/blobs.js +24 -0
- package/esm/types/const-date-time.d.ts +2 -0
- package/esm/types/const-date-time.js +9 -0
- package/esm/types/default-to-zero-value.d.ts +11 -0
- package/esm/types/default-to-zero-value.js +30 -0
- package/esm/types/enums.d.ts +8 -0
- package/esm/types/enums.js +26 -0
- package/esm/types/fp.d.ts +30 -0
- package/esm/types/fp.js +30 -0
- package/esm/types/index.d.ts +9 -0
- package/esm/types/index.js +9 -0
- package/esm/types/operations.d.ts +26 -0
- package/esm/types/operations.js +76 -0
- package/esm/types/primitives.d.ts +8 -0
- package/esm/types/primitives.js +108 -0
- package/esm/types/rfcdate.d.ts +20 -0
- package/esm/types/rfcdate.js +42 -0
- package/esm/types/smart-union.d.ts +6 -0
- package/esm/types/smart-union.js +116 -0
- package/esm/types/streams.d.ts +1 -0
- package/esm/types/streams.js +14 -0
- package/esm/types/unrecognized.d.ts +15 -0
- package/esm/types/unrecognized.js +30 -0
- package/openapi.json +2853 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 State
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# @usestatemachines/sdk
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for [State](https://usestatemachines.com): create disposable environments of
|
|
4
|
+
replica apps, wait until they are ready, connect to them, and clean them up.
|
|
5
|
+
|
|
6
|
+
- Node.js 20.20 or later (22 or 24 recommended), and Cloudflare Workers. ESM only.
|
|
7
|
+
- Retries transient failures safely, including `create`, which is idempotent.
|
|
8
|
+
|
|
9
|
+
## Install and quickstart
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install @usestatemachines/sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { StateMachines } from '@usestatemachines/sdk';
|
|
17
|
+
|
|
18
|
+
// Reads STATE_API_KEY. The workspace comes from the key.
|
|
19
|
+
const sm = new StateMachines();
|
|
20
|
+
|
|
21
|
+
// A failed create carries err.idempotencyKey: see "Recovering a failed create" in the README.
|
|
22
|
+
const environment = await sm.environments.create({
|
|
23
|
+
name: 'quickstart',
|
|
24
|
+
// The default Salesforce app; sm.catalog.get() lists every app.
|
|
25
|
+
apps: [{ key: 'crm', appId: 'salesforce' }],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
await sm.environments.waitUntilReady(environment.id);
|
|
30
|
+
|
|
31
|
+
// Salesforce has several actors (its users) and API profiles, so pick one of each.
|
|
32
|
+
// The connection lists the actors; this acts as the first.
|
|
33
|
+
const { actors } = await sm.environments.getConnection(environment.id, 'crm');
|
|
34
|
+
const [actor] = actors;
|
|
35
|
+
|
|
36
|
+
if (!actor) {
|
|
37
|
+
throw new Error('The Salesforce app has no actors.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const crm = await sm.environments.connect(environment.id, 'crm', {
|
|
41
|
+
actorId: actor.id,
|
|
42
|
+
apiId: 'rest',
|
|
43
|
+
});
|
|
44
|
+
const response = await crm.fetch('/services/data/v67.0/limits');
|
|
45
|
+
|
|
46
|
+
console.log(response.status, await response.json());
|
|
47
|
+
} finally {
|
|
48
|
+
// Stop even when a step fails; lifetime counts from the start.
|
|
49
|
+
await sm.environments.stop(environment.id);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Authentication
|
|
54
|
+
|
|
55
|
+
`new StateMachines()` reads the API key from `STATE_API_KEY`, but only where a global `process`
|
|
56
|
+
exists. Without a key, the constructor throws a `TypeError`.
|
|
57
|
+
|
|
58
|
+
In Cloudflare Workers, pass `apiKey` explicitly:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { StateMachines } from '@usestatemachines/sdk';
|
|
62
|
+
|
|
63
|
+
export default {
|
|
64
|
+
async fetch(_request: Request, env: { STATE_API_KEY: string }) {
|
|
65
|
+
const sm = new StateMachines({ apiKey: env.STATE_API_KEY });
|
|
66
|
+
// Bound the call: retries could otherwise hold the request for minutes.
|
|
67
|
+
const { data } = await sm.environments.list(
|
|
68
|
+
{ status: 'live' },
|
|
69
|
+
{ signal: AbortSignal.timeout(20_000) },
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return Response.json(data.map(({ id, status }) => ({ id, status })));
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Calls that need a workspace (`environments.list` and `environments.create`) use, in order:
|
|
78
|
+
|
|
79
|
+
1. `workspaceId` passed to that call
|
|
80
|
+
2. `workspaceId` passed to the constructor
|
|
81
|
+
3. the workspace of the API key, read once from `sm.credential.get()` and cached
|
|
82
|
+
|
|
83
|
+
A user session that has no workspace can't supply one, so those calls throw and ask for
|
|
84
|
+
`workspaceId`.
|
|
85
|
+
|
|
86
|
+
Other constructor options: `serverURL`, `fetch` (a custom fetch implementation), `httpClient`
|
|
87
|
+
(copied, never changed; not combined with `fetch`) and `timeoutMs` (see below).
|
|
88
|
+
|
|
89
|
+
## Environment lifecycle
|
|
90
|
+
|
|
91
|
+
1. `create(input)` returns the environment as soon as the API accepts it, before the apps boot.
|
|
92
|
+
2. `waitUntilReady(id)` polls until it is `running`. It throws `EnvironmentFailedError` if the
|
|
93
|
+
environment becomes `failed`, `stopping` or `stopped`, and `TimeoutError` after `timeoutMs`
|
|
94
|
+
(default 10 minutes). It **never stops the environment**, even when it throws.
|
|
95
|
+
3. `connect(id, appKey, { actorId, apiId })` returns a `Connection` to one app (see
|
|
96
|
+
[Connections](#connections)).
|
|
97
|
+
4. `stop(id)` stops it. Always stop in a `finally`.
|
|
98
|
+
|
|
99
|
+
`lifetimeMinutes` counts from when the environment starts, so boot time uses part of it. When it
|
|
100
|
+
runs out, the environment stops with `stopReason: 'expired'`. The default and maximum are in
|
|
101
|
+
`(await sm.credential.get()).limits.environmentLifetime`.
|
|
102
|
+
|
|
103
|
+
Both waits accept `{ timeoutMs, signal }`. Aborting the `signal` rejects the wait with
|
|
104
|
+
`signal.reason`. They poll at growing intervals of about 1 to 5 seconds, with random jitter, and
|
|
105
|
+
keep polling through transient failures.
|
|
106
|
+
|
|
107
|
+
## Retries and timeouts
|
|
108
|
+
|
|
109
|
+
Every API call retries on its own, with growing, jittered delays of up to about 5 seconds between
|
|
110
|
+
attempts:
|
|
111
|
+
|
|
112
|
+
| Response | Retried | Why |
|
|
113
|
+
| --- | --- | --- |
|
|
114
|
+
| `502`, `503`, `504` | Yes | The API is saturated (`503`), or Cloudflare can't reach it for a moment (`502`, `504`). |
|
|
115
|
+
| No response: connection error or attempt timeout | Yes | The request may never have arrived. |
|
|
116
|
+
| `429` (`environment-limit`) | No | A quota on live environments. Stop one, then create again. |
|
|
117
|
+
| Any other status: other `4xx`, and other `5xx` | No | Not retried by the SDK. |
|
|
118
|
+
|
|
119
|
+
- A `Retry-After` header is honoured, up to 5 seconds.
|
|
120
|
+
- Retries have a 10-minute budget, checked after each failed attempt. It is not a deadline: the
|
|
121
|
+
last delay and attempt can end later. Pass a `signal` to bound a call: an abort during a
|
|
122
|
+
retry delay takes effect when the delay ends, at most about 5 seconds later.
|
|
123
|
+
- `timeoutMs` (constructor option, default 60 seconds) limits **each attempt**. A timed-out
|
|
124
|
+
attempt is retried with a fresh timeout.
|
|
125
|
+
- A `signal` passed to a call aborts the whole call, including retries. The call rejects with a
|
|
126
|
+
`StateMachinesError` whose `status` is `null`.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const environment = await sm.environments.get('env_123', {
|
|
130
|
+
signal: AbortSignal.timeout(30_000),
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Recovering a failed create
|
|
135
|
+
|
|
136
|
+
Every error thrown by `environments.create` carries `err.idempotencyKey`. Replaying the create
|
|
137
|
+
with that key and the **same input** returns the environment the first call created, instead of
|
|
138
|
+
creating a duplicate. If the first call never reached the API, the replay creates it. Replaying
|
|
139
|
+
with a different input fails with `idempotency-conflict`. Keys are kept for a day.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { StateMachines, StateMachinesError } from '@usestatemachines/sdk';
|
|
143
|
+
|
|
144
|
+
const sm = new StateMachines();
|
|
145
|
+
const input = { name: 'nightly', apps: [{ key: 'crm', appId: 'salesforce' }] };
|
|
146
|
+
|
|
147
|
+
async function createOnce() {
|
|
148
|
+
try {
|
|
149
|
+
return await sm.environments.create(input);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
// Safe after any failure: the replay can't create a second environment.
|
|
152
|
+
if (error instanceof StateMachinesError && error.idempotencyKey) {
|
|
153
|
+
return sm.environments.create(input, { idempotencyKey: error.idempotencyKey });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const environment = await createOnce();
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Cleanup
|
|
164
|
+
|
|
165
|
+
`stop` returns as soon as the API accepts it. To confirm cleanup, call `waitUntilStopped`. It
|
|
166
|
+
resolves only when:
|
|
167
|
+
|
|
168
|
+
- the environment is `stopped` or `failed`, **and**
|
|
169
|
+
- every app's `disposal` is `complete`.
|
|
170
|
+
|
|
171
|
+
If any app's disposal is `failed` or `unknown`, it throws `DisposalIncompleteError` with the
|
|
172
|
+
environment. We recommend it in CI, so a run fails when cleanup can't be confirmed:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
await sm.environments.stop(environment.id);
|
|
176
|
+
await sm.environments.waitUntilStopped(environment.id);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Errors
|
|
180
|
+
|
|
181
|
+
Every failed API call throws a `StateMachinesError`:
|
|
182
|
+
|
|
183
|
+
| Field | Type | Meaning |
|
|
184
|
+
| --- | --- | --- |
|
|
185
|
+
| `status` | `number \| null` | HTTP status; `null` when there was no parsed response. |
|
|
186
|
+
| `code` | `ErrorCode \| null` | API error code, such as `environment-limit`. |
|
|
187
|
+
| `requestId` | `string \| null` | Include it when you contact support. |
|
|
188
|
+
| `fields` | `Record<string, string[]> \| null` | Per-field validation messages. |
|
|
189
|
+
| `retryAfter` | `number \| null` | Milliseconds the API asked you to wait. |
|
|
190
|
+
| `retryable` | `boolean` | `true` for a `502`, `503` or `504`, a connection error or an attempt timeout. `false` for caller aborts, request validation errors and responses that aren't valid JSON. |
|
|
191
|
+
| `idempotencyKey` | `string \| undefined` | Set on `environments.create` failures. |
|
|
192
|
+
|
|
193
|
+
`status` is `null` when no HTTP response was received, and also when a response wasn't valid JSON.
|
|
194
|
+
Error messages never include response bodies. The waits throw their own classes:
|
|
195
|
+
`EnvironmentFailedError` and `DisposalIncompleteError` (both with `environment`) and
|
|
196
|
+
`TimeoutError` (with `timeoutMs` and the last `environment` seen, or `null`). Check errors with
|
|
197
|
+
`instanceof`, not `name`: a platform abort is also named `TimeoutError`.
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
import { ErrorCode, StateMachinesError } from '@usestatemachines/sdk';
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
await sm.environments.create(input);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error instanceof StateMachinesError && error.code === ErrorCode.EnvironmentLimit) {
|
|
206
|
+
console.error('Too many live environments; stop one first.');
|
|
207
|
+
} else {
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Pagination
|
|
214
|
+
|
|
215
|
+
`environments.list`, `environments.events` and `workspaces.events` return one page,
|
|
216
|
+
`{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
let cursor: string | undefined;
|
|
220
|
+
|
|
221
|
+
do {
|
|
222
|
+
const page = await sm.environments.list({ status: 'live', cursor });
|
|
223
|
+
|
|
224
|
+
for (const environment of page.data) {
|
|
225
|
+
console.log(environment.id, environment.status);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
cursor = page.nextCursor ?? undefined;
|
|
229
|
+
} while (cursor);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Connections
|
|
233
|
+
|
|
234
|
+
`connect(id, appKey)` requires the app to be ready. If the app has several actors, pass
|
|
235
|
+
`{ actorId }`; if it has several API profiles, pass `{ apiId }`. Salesforce has both, so pass both.
|
|
236
|
+
Otherwise it throws a `TypeError` that names the choices. It returns a `Connection` with `baseUrl`, `environmentId`, `appKey`, `actorId`, `api` and
|
|
237
|
+
`expiresAt`, and these ways to call the app:
|
|
238
|
+
|
|
239
|
+
- `headers()` returns `X-State-Environment-Token` plus the app's actor header, such as
|
|
240
|
+
`Authorization: Bearer <token>`. For an API that takes the actor token in the request body,
|
|
241
|
+
such as SOAP, it returns only `X-State-Environment-Token`.
|
|
242
|
+
- `fetch(path, init)` sends one request with those headers and returns the raw `Response`. A
|
|
243
|
+
leading `/` means the root of `baseUrl`. It:
|
|
244
|
+
- only reaches URLs under `baseUrl`, and throws for any other path
|
|
245
|
+
- never follows redirects (`redirect: 'manual'`)
|
|
246
|
+
- never sends your API key
|
|
247
|
+
- never retries
|
|
248
|
+
- rejects `x-state-*` headers from the caller
|
|
249
|
+
- `actorToken()` returns the actor token itself, for APIs that take it in the request body, such
|
|
250
|
+
as SOAP. Treat it as a secret.
|
|
251
|
+
|
|
252
|
+
`formatActorHeader(actorHeader, token)` returns the `{ name, value }` for an API profile's actor
|
|
253
|
+
header: `Bearer <token>` for `Authorization`, and the raw token for any other header. It returns
|
|
254
|
+
`null` for a SOAP profile (`SOAP SessionHeader`), whose token goes in the envelope instead.
|
|
255
|
+
|
|
256
|
+
**SOAP APIs:** Salesforce's `soap-partner` profile, when the release offers it (the default
|
|
257
|
+
release does), takes the actor token in the envelope's `SessionHeader/sessionId`. XML-escape any
|
|
258
|
+
value you put in the envelope:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
const soap = await sm.environments.connect(environment.id, 'crm', {
|
|
262
|
+
actorId: actor.id, // from getConnection, as in the quickstart
|
|
263
|
+
apiId: 'soap-partner',
|
|
264
|
+
});
|
|
265
|
+
const envelope = `<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns="urn:partner.soap.sforce.com">
|
|
266
|
+
<env:Header><SessionHeader><sessionId>${soap.actorToken()}</sessionId></SessionHeader></env:Header>
|
|
267
|
+
<env:Body><undelete><ids>001000000000000AAA</ids></undelete></env:Body>
|
|
268
|
+
</env:Envelope>`;
|
|
269
|
+
const response = await soap.fetch('/services/Soap/u/67.0', {
|
|
270
|
+
method: 'POST',
|
|
271
|
+
headers: { 'Content-Type': 'text/xml', SOAPAction: '""' },
|
|
272
|
+
body: envelope,
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Vendor clients:** the gateway needs `X-State-Environment-Token` on every request. A client such
|
|
277
|
+
as jsforce only sends it if you configure custom headers. Prefer `Connection.fetch`. If you pass
|
|
278
|
+
`headers()` to another client, those headers carry live tokens: only send them to `baseUrl`, and
|
|
279
|
+
don't let the client follow redirects, which would forward the tokens to another host:
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
const crm = await sm.environments.connect(environment.id, 'crm', {
|
|
283
|
+
actorId: actor.id, // from getConnection, as in the quickstart
|
|
284
|
+
apiId: 'rest',
|
|
285
|
+
});
|
|
286
|
+
const response = await fetch(new URL('services/data/v67.0/limits', crm.baseUrl), {
|
|
287
|
+
headers: crm.headers(),
|
|
288
|
+
redirect: 'manual',
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Supported runtimes
|
|
293
|
+
|
|
294
|
+
- **Node.js 20.20 or later**, with 22 or 24 recommended: Node 20 is past its end of life, and
|
|
295
|
+
earlier Node 20 releases fail when retrying a `POST`.
|
|
296
|
+
- **Cloudflare Workers.**
|
|
297
|
+
- **Not browsers.** Never ship an API key to a browser.
|
|
298
|
+
|
|
299
|
+
TypeScript consumers need `lib: ["DOM"]` or `@types/node`, for the `fetch`, `Request` and
|
|
300
|
+
`Response` types.
|
|
301
|
+
|
|
302
|
+
## Compatibility
|
|
303
|
+
|
|
304
|
+
- While the version is `0.x`, a minor release may include breaking changes. Pin the minor
|
|
305
|
+
version.
|
|
306
|
+
- The API may add error codes and enum values, such as a new status or stop reason. Enums are
|
|
307
|
+
open: handle values you don't recognise.
|
|
308
|
+
|
|
309
|
+
## License
|
|
310
|
+
|
|
311
|
+
MIT
|
package/agents/SKILL.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
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 - creating environments, connecting as an actor, 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
|
+
---
|
|
5
|
+
|
|
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`.
|
|
7
|
+
|
|
8
|
+
## Workflow
|
|
9
|
+
|
|
10
|
+
1. 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.
|
|
11
|
+
2. Create the environment with only the apps the task needs. Log `await sm.catalog.get()` for each app's `appId` and the actors and API profiles its release offers; don't guess IDs. `key` is your own name for the app, and later calls take it.
|
|
12
|
+
3. Open a `try` right after `create` returns, and call `stop` in its `finally`.
|
|
13
|
+
4. `await sm.environments.waitUntilReady(environment.id)`. It never stops the environment.
|
|
14
|
+
5. Connect as one actor through one API profile. Salesforce has several of each, so name both: `sm.environments.connect(environment.id, 'crm', { actorId: actor.id, apiId: 'rest' })`, taking `actor` from `(await sm.environments.getConnection(environment.id, 'crm')).actors`. Salesforce actor IDs are its user IDs, such as `005…`, never a role name, so read them from the connection instead of hard-coding one. Choose the actor whose permissions the task needs; each release in the catalog lists them.
|
|
15
|
+
6. Call the app with the connection's `fetch`. A leading `/` means the root of `baseUrl`.
|
|
16
|
+
7. In `finally`, `await sm.environments.stop(environment.id)`. In CI, then await `sm.environments.waitUntilStopped(environment.id)`.
|
|
17
|
+
|
|
18
|
+
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`.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { StateMachines } from '@usestatemachines/sdk';
|
|
22
|
+
|
|
23
|
+
// Reads STATE_API_KEY. The workspace comes from the key.
|
|
24
|
+
const sm = new StateMachines();
|
|
25
|
+
|
|
26
|
+
// A failed create carries err.idempotencyKey: see "Recovering a failed create" in the README.
|
|
27
|
+
const environment = await sm.environments.create({
|
|
28
|
+
name: 'quickstart',
|
|
29
|
+
// The default Salesforce app; sm.catalog.get() lists every app.
|
|
30
|
+
apps: [{ key: 'crm', appId: 'salesforce' }],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await sm.environments.waitUntilReady(environment.id);
|
|
35
|
+
|
|
36
|
+
// Salesforce has several actors (its users) and API profiles, so pick one of each.
|
|
37
|
+
// The connection lists the actors; this acts as the first.
|
|
38
|
+
const { actors } = await sm.environments.getConnection(environment.id, 'crm');
|
|
39
|
+
const [actor] = actors;
|
|
40
|
+
|
|
41
|
+
if (!actor) {
|
|
42
|
+
throw new Error('The Salesforce app has no actors.');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const crm = await sm.environments.connect(environment.id, 'crm', {
|
|
46
|
+
actorId: actor.id,
|
|
47
|
+
apiId: 'rest',
|
|
48
|
+
});
|
|
49
|
+
const response = await crm.fetch('/services/data/v67.0/limits');
|
|
50
|
+
|
|
51
|
+
console.log(response.status, await response.json());
|
|
52
|
+
} finally {
|
|
53
|
+
// Stop even when a step fails; lifetime counts from the start.
|
|
54
|
+
await sm.environments.stop(environment.id);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Rules
|
|
59
|
+
|
|
60
|
+
- Always stop in `finally`. `waitUntilStopped` confirms every app was disposed; use it in CI so a run fails when cleanup can't be confirmed.
|
|
61
|
+
- 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 `create` for longer runs.
|
|
62
|
+
- Live environments are capped per organization: `limits.liveEnvironments` from `sm.credential.get()`. Stop the ones you no longer need.
|
|
63
|
+
- Never print, log or commit `STATE_API_KEY`, tokens, or `headers()` or `actorToken()` output.
|
|
64
|
+
- 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`.
|
|
65
|
+
- `connection.fetch` never sends your API key, never follows redirects and only reaches URLs under `baseUrl`.
|
|
66
|
+
- 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.
|
|
67
|
+
- 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.
|
|
68
|
+
- `environments.list`, `environments.events` and `workspaces.events` return one page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`.
|
|
69
|
+
- Check errors with `instanceof` (`StateMachinesError`, `EnvironmentFailedError`, `DisposalIncompleteError`, `TimeoutError`), not by `name`.
|
|
70
|
+
|
|
71
|
+
## Recovering a failed create
|
|
72
|
+
|
|
73
|
+
The SDK already retries `502`, `503`, `504` and connection errors with one idempotency key. If `create` still throws, don't call it again as a new request: that can start a second environment. Every error from `create` carries `err.idempotencyKey`. Call `sm.environments.create(input, { idempotencyKey: err.idempotencyKey })` with the same `input`. 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.
|
|
74
|
+
|
|
75
|
+
## Troubleshooting
|
|
76
|
+
|
|
77
|
+
| Symptom | Cause | Fix |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| `release-unavailable`, 422 | Unknown or retired `appId` or `appReleaseId`. | Check the IDs against `sm.catalog.get()`. Omit `appReleaseId` for the default release. |
|
|
80
|
+
| `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. |
|
|
81
|
+
| `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 create again. |
|
|
82
|
+
| `lifetime-too-long`, 422 | `lifetimeMinutes` is above the maximum. | Use at most `limits.environmentLifetime.maximumMinutes`. |
|
|
83
|
+
| `TypeError: Choose one actor: ...` or `Choose one API: ...` | The app has several actors or API profiles, or the ID doesn't exist. The message lists the valid IDs. | Pass both `actorId` and `apiId` from the listed IDs. |
|
|
84
|
+
| `TypeError: App ... is not ready to connect` | connect() ran before the app was `ready`. | Await `waitUntilReady` first. |
|
|
85
|
+
| `connection-unavailable`, 409 | The app stopped, or the environment is no longer `running`. | Check `sm.environments.get(environment.id)`; if it stopped or expired, create a new one. |
|
|
86
|
+
| `EnvironmentFailedError` from `waitUntilReady` | The environment became `failed`, `stopping` or `stopped`. | Read `err.environment.stopReason` and each app's `errorCode`. Still stop it in `finally`. |
|
|
87
|
+
| 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. |
|
|
88
|
+
| `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`. |
|
|
89
|
+
|
|
90
|
+
More: https://state-web.usestatemachines.workers.dev/llms.txt and https://www.npmjs.com/package/@usestatemachines/sdk
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Generated by scripts/build-agents.mjs.
|
|
2
|
+
|
|
3
|
+
export declare const prompt: string;
|
|
4
|
+
|
|
5
|
+
export declare const llmsTxt: string;
|
|
6
|
+
|
|
7
|
+
export declare const skill: string;
|
|
8
|
+
|
|
9
|
+
export declare const quickstart: string;
|
|
10
|
+
|
|
11
|
+
export declare const templates: Readonly<
|
|
12
|
+
Record<'prompt' | 'llmsTxt' | 'skill', string>
|
|
13
|
+
>;
|
package/agents/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Generated by scripts/build-agents.mjs from templates/ and examples/quickstart.ts.
|
|
2
|
+
|
|
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. Never print, log or commit keys or tokens, including `connection.headers()` and `connection.actorToken()`, which return live tokens. 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:\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\n// Reads STATE_API_KEY. The workspace comes from the key.\nconst sm = new StateMachines();\n\n// A failed create carries err.idempotencyKey: see \"Recovering a failed create\" in the README.\nconst environment = await sm.environments.create({\n name: 'quickstart',\n // The default Salesforce app; sm.catalog.get() lists every app.\n apps: [{ key: 'crm', appId: 'salesforce' }],\n});\n\ntry {\n await sm.environments.waitUntilReady(environment.id);\n\n // Salesforce has several actors (its users) and API profiles, so pick one of each.\n // The connection lists the actors; this acts as the first.\n const { actors } = await sm.environments.getConnection(environment.id, 'crm');\n const [actor] = actors;\n\n if (!actor) {\n throw new Error('The Salesforce app has no actors.');\n }\n\n const crm = await sm.environments.connect(environment.id, 'crm', {\n actorId: actor.id,\n apiId: 'rest',\n });\n const response = await crm.fetch('/services/data/v67.0/limits');\n\n console.log(response.status, await response.json());\n} finally {\n // Stop even when a step fails; lifetime counts from the start.\n await sm.environments.stop(environment.id);\n}\n```\n\n4. Adapt it to my task. Log `await sm.catalog.get()` to find each app's `appId` and the actors and API profiles its release offers; don't guess IDs. Create only the apps the task needs, and pass connect() the `actorId` and `apiId` the task needs.\n5. Always stop the environment in a `finally`. In CI, then await `sm.environments.waitUntilStopped(environment.id)` so the run fails if cleanup isn't confirmed.\n6. The SDK retries `502`, `503`, `504` and connection errors itself. If `create` still throws, call `sm.environments.create(input, { idempotencyKey: err.idempotencyKey })` with the same `input`. Never retry `create` without the key: that can start a second environment.\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
|
+
|
|
6
|
+
export const llmsTxt =
|
|
7
|
+
"# State Machines\n\n> State Machines runs disposable environments of replica SaaS apps, such as Salesforce, for tests and coding agents. Create, connect to 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. Create it, wait until it is `running`, connect, then stop it.\n- App key and app ID: in `create`, `apps: [{ key: 'crm', appId: 'salesforce' }]`. `appId` names the app in the catalog (`sm.catalog.get()`); `key` is your own name for it in this environment, and later calls take the key.\n- Actor: who the app sees you as. API profile: one API of the app, with its path and versions. Each release in the catalog lists both.\n- Connection: connect() needs the app `ready`. Salesforce has several actors and API profiles, so name both: `sm.environments.connect(environment.id, 'crm', { actorId: actor.id, apiId: 'rest' })`, taking `actor` from `(await sm.environments.getConnection(environment.id, 'crm')).actors`. Salesforce actor IDs are its user IDs, such as `005…`, never a role name; read them from the connection instead of hard-coding one. `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- 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` and `workspaces.events` 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- `waitUntilReady` throws `EnvironmentFailedError` if the environment becomes `failed`, `stopping` or `stopped`, and `TimeoutError` after 10 minutes by default. It never stops the environment.\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 `stop` in a `finally`. In CI, then await `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`. Every `create` error carries `err.idempotencyKey`: `sm.environments.create(input, { idempotencyKey: err.idempotencyKey })` with the same input returns the environment the first call created.\n\n| Code | Status | What to do |\n| --- | --- | --- |\n| `environment-limit` | 429 | Not retried. Stop a live environment (`sm.environments.list({ status: 'live' })`), then create 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, create a new one. (Before that, connect() throws a `TypeError` for an app that isn't ready or an unknown actor or API.) |\n| `idempotency-conflict` | 409 | The idempotency key was used with a different input. Replay with the exact same input. |\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\n// Reads STATE_API_KEY. The workspace comes from the key.\nconst sm = new StateMachines();\n\n// A failed create carries err.idempotencyKey: see \"Recovering a failed create\" in the README.\nconst environment = await sm.environments.create({\n name: 'quickstart',\n // The default Salesforce app; sm.catalog.get() lists every app.\n apps: [{ key: 'crm', appId: 'salesforce' }],\n});\n\ntry {\n await sm.environments.waitUntilReady(environment.id);\n\n // Salesforce has several actors (its users) and API profiles, so pick one of each.\n // The connection lists the actors; this acts as the first.\n const { actors } = await sm.environments.getConnection(environment.id, 'crm');\n const [actor] = actors;\n\n if (!actor) {\n throw new Error('The Salesforce app has no actors.');\n }\n\n const crm = await sm.environments.connect(environment.id, 'crm', {\n actorId: actor.id,\n apiId: 'rest',\n });\n const response = await crm.fetch('/services/data/v67.0/limits');\n\n console.log(response.status, await response.json());\n} finally {\n // Stop even when a step fails; lifetime counts from the start.\n await sm.environments.stop(environment.id);\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
|
+
|
|
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 - creating environments, connecting as an actor, 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. Create the environment with only the apps the task needs. Log `await sm.catalog.get()` for each app's `appId` and the actors and API profiles its release offers; don't guess IDs. `key` is your own name for the app, and later calls take it.\n3. Open a `try` right after `create` returns, and call `stop` in its `finally`.\n4. `await sm.environments.waitUntilReady(environment.id)`. It never stops the environment.\n5. Connect as one actor through one API profile. Salesforce has several of each, so name both: `sm.environments.connect(environment.id, 'crm', { actorId: actor.id, apiId: 'rest' })`, taking `actor` from `(await sm.environments.getConnection(environment.id, 'crm')).actors`. Salesforce actor IDs are its user IDs, such as `005…`, never a role name, so read them from the connection instead of hard-coding one. Choose the actor whose permissions the task needs; each release in the catalog lists them.\n6. Call the app with the connection's `fetch`. A leading `/` means the root of `baseUrl`.\n7. In `finally`, `await sm.environments.stop(environment.id)`. In CI, then await `sm.environments.waitUntilStopped(environment.id)`.\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\n// Reads STATE_API_KEY. The workspace comes from the key.\nconst sm = new StateMachines();\n\n// A failed create carries err.idempotencyKey: see \"Recovering a failed create\" in the README.\nconst environment = await sm.environments.create({\n name: 'quickstart',\n // The default Salesforce app; sm.catalog.get() lists every app.\n apps: [{ key: 'crm', appId: 'salesforce' }],\n});\n\ntry {\n await sm.environments.waitUntilReady(environment.id);\n\n // Salesforce has several actors (its users) and API profiles, so pick one of each.\n // The connection lists the actors; this acts as the first.\n const { actors } = await sm.environments.getConnection(environment.id, 'crm');\n const [actor] = actors;\n\n if (!actor) {\n throw new Error('The Salesforce app has no actors.');\n }\n\n const crm = await sm.environments.connect(environment.id, 'crm', {\n actorId: actor.id,\n apiId: 'rest',\n });\n const response = await crm.fetch('/services/data/v67.0/limits');\n\n console.log(response.status, await response.json());\n} finally {\n // Stop even when a step fails; lifetime counts from the start.\n await sm.environments.stop(environment.id);\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- 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 `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` and `workspaces.events` return one page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`.\n- Check errors with `instanceof` (`StateMachinesError`, `EnvironmentFailedError`, `DisposalIncompleteError`, `TimeoutError`), 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 `create` still throws, don't call it again as a new request: that can start a second environment. Every error from `create` carries `err.idempotencyKey`. Call `sm.environments.create(input, { idempotencyKey: err.idempotencyKey })` with the same `input`. 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`. | 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 create again. |\n| `lifetime-too-long`, 422 | `lifetimeMinutes` is above the maximum. | Use at most `limits.environmentLifetime.maximumMinutes`. |\n| `TypeError: Choose one actor: ...` or `Choose one API: ...` | The app has several actors or API profiles, or the ID doesn't exist. The message lists the valid IDs. | Pass both `actorId` and `apiId` from the listed IDs. |\n| `TypeError: App ... is not ready to connect` | connect() ran before the app was `ready`. | Await `waitUntilReady` first. |\n| `connection-unavailable`, 409 | The app stopped, or the environment is no longer `running`. | Check `sm.environments.get(environment.id)`; if it stopped or expired, create a new one. |\n| `EnvironmentFailedError` from `waitUntilReady` | 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| `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
|
+
|
|
12
|
+
export const quickstart =
|
|
13
|
+
"import { StateMachines } from '@usestatemachines/sdk';\n\n// Reads STATE_API_KEY. The workspace comes from the key.\nconst sm = new StateMachines();\n\n// A failed create carries err.idempotencyKey: see \"Recovering a failed create\" in the README.\nconst environment = await sm.environments.create({\n name: 'quickstart',\n // The default Salesforce app; sm.catalog.get() lists every app.\n apps: [{ key: 'crm', appId: 'salesforce' }],\n});\n\ntry {\n await sm.environments.waitUntilReady(environment.id);\n\n // Salesforce has several actors (its users) and API profiles, so pick one of each.\n // The connection lists the actors; this acts as the first.\n const { actors } = await sm.environments.getConnection(environment.id, 'crm');\n const [actor] = actors;\n\n if (!actor) {\n throw new Error('The Salesforce app has no actors.');\n }\n\n const crm = await sm.environments.connect(environment.id, 'crm', {\n actorId: actor.id,\n apiId: 'rest',\n });\n const response = await crm.fetch('/services/data/v67.0/limits');\n\n console.log(response.status, await response.json());\n} finally {\n // Stop even when a step fails; lifetime counts from the start.\n await sm.environments.stop(environment.id);\n}\n";
|
|
14
|
+
|
|
15
|
+
export const templates = Object.freeze({
|
|
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. Never print, log or commit keys or tokens, including `connection.headers()` and `connection.actorToken()`, which return live tokens. 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:\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\n// Reads STATE_API_KEY. The workspace comes from the key.\nconst sm = new StateMachines();\n\n// A failed create carries err.idempotencyKey: see \"Recovering a failed create\" in the README.\nconst environment = await sm.environments.create({\n name: 'quickstart',\n // The default Salesforce app; sm.catalog.get() lists every app.\n apps: [{ key: 'crm', appId: 'salesforce' }],\n});\n\ntry {\n await sm.environments.waitUntilReady(environment.id);\n\n // Salesforce has several actors (its users) and API profiles, so pick one of each.\n // The connection lists the actors; this acts as the first.\n const { actors } = await sm.environments.getConnection(environment.id, 'crm');\n const [actor] = actors;\n\n if (!actor) {\n throw new Error('The Salesforce app has no actors.');\n }\n\n const crm = await sm.environments.connect(environment.id, 'crm', {\n actorId: actor.id,\n apiId: 'rest',\n });\n const response = await crm.fetch('/services/data/v67.0/limits');\n\n console.log(response.status, await response.json());\n} finally {\n // Stop even when a step fails; lifetime counts from the start.\n await sm.environments.stop(environment.id);\n}\n```\n\n4. Adapt it to my task. Log `await sm.catalog.get()` to find each app's `appId` and the actors and API profiles its release offers; don't guess IDs. Create only the apps the task needs, and pass connect() the `actorId` and `apiId` the task needs.\n5. Always stop the environment in a `finally`. In CI, then await `sm.environments.waitUntilStopped(environment.id)` so the run fails if cleanup isn't confirmed.\n6. The SDK retries `502`, `503`, `504` and connection errors itself. If `create` still throws, call `sm.environments.create(input, { idempotencyKey: err.idempotencyKey })` with the same `input`. Never retry `create` without the key: that can start a second environment.\n\nRead more: `node_modules/@usestatemachines/sdk/README.md`, {{origin}}/llms.txt and https://www.npmjs.com/package/@usestatemachines/sdk\n",
|
|
18
|
+
llmsTxt:
|
|
19
|
+
"# State Machines\n\n> State Machines runs disposable environments of replica SaaS apps, such as Salesforce, for tests and coding agents. Create, connect to 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. Create it, wait until it is `running`, connect, then stop it.\n- App key and app ID: in `create`, `apps: [{ key: 'crm', appId: 'salesforce' }]`. `appId` names the app in the catalog (`sm.catalog.get()`); `key` is your own name for it in this environment, and later calls take the key.\n- Actor: who the app sees you as. API profile: one API of the app, with its path and versions. Each release in the catalog lists both.\n- Connection: connect() needs the app `ready`. Salesforce has several actors and API profiles, so name both: `sm.environments.connect(environment.id, 'crm', { actorId: actor.id, apiId: 'rest' })`, taking `actor` from `(await sm.environments.getConnection(environment.id, 'crm')).actors`. Salesforce actor IDs are its user IDs, such as `005…`, never a role name; read them from the connection instead of hard-coding one. `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- 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` and `workspaces.events` 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- `waitUntilReady` throws `EnvironmentFailedError` if the environment becomes `failed`, `stopping` or `stopped`, and `TimeoutError` after 10 minutes by default. It never stops the environment.\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 `stop` in a `finally`. In CI, then await `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`. Every `create` error carries `err.idempotencyKey`: `sm.environments.create(input, { idempotencyKey: err.idempotencyKey })` with the same input returns the environment the first call created.\n\n| Code | Status | What to do |\n| --- | --- | --- |\n| `environment-limit` | 429 | Not retried. Stop a live environment (`sm.environments.list({ status: 'live' })`), then create 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, create a new one. (Before that, connect() throws a `TypeError` for an app that isn't ready or an unknown actor or API.) |\n| `idempotency-conflict` | 409 | The idempotency key was used with a different input. Replay with the exact same input. |\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\n// Reads STATE_API_KEY. The workspace comes from the key.\nconst sm = new StateMachines();\n\n// A failed create carries err.idempotencyKey: see \"Recovering a failed create\" in the README.\nconst environment = await sm.environments.create({\n name: 'quickstart',\n // The default Salesforce app; sm.catalog.get() lists every app.\n apps: [{ key: 'crm', appId: 'salesforce' }],\n});\n\ntry {\n await sm.environments.waitUntilReady(environment.id);\n\n // Salesforce has several actors (its users) and API profiles, so pick one of each.\n // The connection lists the actors; this acts as the first.\n const { actors } = await sm.environments.getConnection(environment.id, 'crm');\n const [actor] = actors;\n\n if (!actor) {\n throw new Error('The Salesforce app has no actors.');\n }\n\n const crm = await sm.environments.connect(environment.id, 'crm', {\n actorId: actor.id,\n apiId: 'rest',\n });\n const response = await crm.fetch('/services/data/v67.0/limits');\n\n console.log(response.status, await response.json());\n} finally {\n // Stop even when a step fails; lifetime counts from the start.\n await sm.environments.stop(environment.id);\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
|
+
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 - creating environments, connecting as an actor, 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. Create the environment with only the apps the task needs. Log `await sm.catalog.get()` for each app's `appId` and the actors and API profiles its release offers; don't guess IDs. `key` is your own name for the app, and later calls take it.\n3. Open a `try` right after `create` returns, and call `stop` in its `finally`.\n4. `await sm.environments.waitUntilReady(environment.id)`. It never stops the environment.\n5. Connect as one actor through one API profile. Salesforce has several of each, so name both: `sm.environments.connect(environment.id, 'crm', { actorId: actor.id, apiId: 'rest' })`, taking `actor` from `(await sm.environments.getConnection(environment.id, 'crm')).actors`. Salesforce actor IDs are its user IDs, such as `005…`, never a role name, so read them from the connection instead of hard-coding one. Choose the actor whose permissions the task needs; each release in the catalog lists them.\n6. Call the app with the connection's `fetch`. A leading `/` means the root of `baseUrl`.\n7. In `finally`, `await sm.environments.stop(environment.id)`. In CI, then await `sm.environments.waitUntilStopped(environment.id)`.\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\n// Reads STATE_API_KEY. The workspace comes from the key.\nconst sm = new StateMachines();\n\n// A failed create carries err.idempotencyKey: see \"Recovering a failed create\" in the README.\nconst environment = await sm.environments.create({\n name: 'quickstart',\n // The default Salesforce app; sm.catalog.get() lists every app.\n apps: [{ key: 'crm', appId: 'salesforce' }],\n});\n\ntry {\n await sm.environments.waitUntilReady(environment.id);\n\n // Salesforce has several actors (its users) and API profiles, so pick one of each.\n // The connection lists the actors; this acts as the first.\n const { actors } = await sm.environments.getConnection(environment.id, 'crm');\n const [actor] = actors;\n\n if (!actor) {\n throw new Error('The Salesforce app has no actors.');\n }\n\n const crm = await sm.environments.connect(environment.id, 'crm', {\n actorId: actor.id,\n apiId: 'rest',\n });\n const response = await crm.fetch('/services/data/v67.0/limits');\n\n console.log(response.status, await response.json());\n} finally {\n // Stop even when a step fails; lifetime counts from the start.\n await sm.environments.stop(environment.id);\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- 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 `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` and `workspaces.events` return one page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`.\n- Check errors with `instanceof` (`StateMachinesError`, `EnvironmentFailedError`, `DisposalIncompleteError`, `TimeoutError`), 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 `create` still throws, don't call it again as a new request: that can start a second environment. Every error from `create` carries `err.idempotencyKey`. Call `sm.environments.create(input, { idempotencyKey: err.idempotencyKey })` with the same `input`. 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`. | 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 create again. |\n| `lifetime-too-long`, 422 | `lifetimeMinutes` is above the maximum. | Use at most `limits.environmentLifetime.maximumMinutes`. |\n| `TypeError: Choose one actor: ...` or `Choose one API: ...` | The app has several actors or API profiles, or the ID doesn't exist. The message lists the valid IDs. | Pass both `actorId` and `apiId` from the listed IDs. |\n| `TypeError: App ... is not ready to connect` | connect() ran before the app was `ready`. | Await `waitUntilReady` first. |\n| `connection-unavailable`, 409 | The app stopped, or the environment is no longer `running`. | Check `sm.environments.get(environment.id)`; if it stopped or expired, create a new one. |\n| `EnvironmentFailedError` from `waitUntilReady` | 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| `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
|
+
});
|
package/agents/llms.txt
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# State Machines
|
|
2
|
+
|
|
3
|
+
> State Machines runs disposable environments of replica SaaS apps, such as Salesforce, for tests and coding agents. Create, connect to and stop them with the TypeScript SDK `@usestatemachines/sdk`.
|
|
4
|
+
|
|
5
|
+
Install 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.
|
|
6
|
+
|
|
7
|
+
## Concepts
|
|
8
|
+
|
|
9
|
+
- Environment: one disposable run of one or more apps. Create it, wait until it is `running`, connect, then stop it.
|
|
10
|
+
- App key and app ID: in `create`, `apps: [{ key: 'crm', appId: 'salesforce' }]`. `appId` names the app in the catalog (`sm.catalog.get()`); `key` is your own name for it in this environment, and later calls take the key.
|
|
11
|
+
- Actor: who the app sees you as. API profile: one API of the app, with its path and versions. Each release in the catalog lists both.
|
|
12
|
+
- Connection: connect() needs the app `ready`. Salesforce has several actors and API profiles, so name both: `sm.environments.connect(environment.id, 'crm', { actorId: actor.id, apiId: 'rest' })`, taking `actor` from `(await sm.environments.getConnection(environment.id, 'crm')).actors`. Salesforce actor IDs are its user IDs, such as `005…`, never a role name; read them from the connection instead of hard-coding one. `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`.
|
|
13
|
+
- 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.
|
|
14
|
+
- Pagination: `environments.list`, `environments.events` and `workspaces.events` return one page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`.
|
|
15
|
+
|
|
16
|
+
## Statuses
|
|
17
|
+
|
|
18
|
+
Enums are open: handle values you don't recognise.
|
|
19
|
+
|
|
20
|
+
- Environment `status`: `queued`, `starting`, `running`, `stopping`, `stopped`, `failed`. `stopReason`: `requested`, `expired`, `app_failed`, `owner_lost`.
|
|
21
|
+
- App `status`: `pending`, `starting`, `ready`, `stopping`, `stopped`, `failed`. A failed app carries `errorCode`.
|
|
22
|
+
- App `disposal`: `pending`, `complete`, `failed`, `unknown`.
|
|
23
|
+
- `waitUntilReady` throws `EnvironmentFailedError` if the environment becomes `failed`, `stopping` or `stopped`, and `TimeoutError` after 10 minutes by default. It never stops the environment.
|
|
24
|
+
- `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`.
|
|
25
|
+
|
|
26
|
+
## Limits
|
|
27
|
+
|
|
28
|
+
- Live environments are capped per organization: `(await sm.credential.get()).limits.liveEnvironments`.
|
|
29
|
+
- `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'`.
|
|
30
|
+
- Always call `stop` in a `finally`. In CI, then await `waitUntilStopped`.
|
|
31
|
+
|
|
32
|
+
## Errors and retries
|
|
33
|
+
|
|
34
|
+
A 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`. Every `create` error carries `err.idempotencyKey`: `sm.environments.create(input, { idempotencyKey: err.idempotencyKey })` with the same input returns the environment the first call created.
|
|
35
|
+
|
|
36
|
+
| Code | Status | What to do |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| `environment-limit` | 429 | Not retried. Stop a live environment (`sm.environments.list({ status: 'live' })`), then create again. |
|
|
39
|
+
| `lifetime-too-long` | 422 | Lower `lifetimeMinutes` to at most `limits.environmentLifetime.maximumMinutes`. |
|
|
40
|
+
| `release-unavailable` | 422 | Unknown or retired app or release. Check `appId` and `appReleaseId` against `sm.catalog.get()`. |
|
|
41
|
+
| `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`. |
|
|
42
|
+
| `connection-unavailable` | 409 | The app stopped or the environment is no longer `running`. If it stopped or expired, create a new one. (Before that, connect() throws a `TypeError` for an app that isn't ready or an unknown actor or API.) |
|
|
43
|
+
| `idempotency-conflict` | 409 | The idempotency key was used with a different input. Replay with the exact same input. |
|
|
44
|
+
| `request-timeout` | 503 | Retried automatically. If `create` still fails, replay it with `err.idempotencyKey`. |
|
|
45
|
+
|
|
46
|
+
## Quickstart
|
|
47
|
+
|
|
48
|
+
Save it unchanged as `state-quickstart.mts` and run `npx tsx state-quickstart.mts`.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { StateMachines } from '@usestatemachines/sdk';
|
|
52
|
+
|
|
53
|
+
// Reads STATE_API_KEY. The workspace comes from the key.
|
|
54
|
+
const sm = new StateMachines();
|
|
55
|
+
|
|
56
|
+
// A failed create carries err.idempotencyKey: see "Recovering a failed create" in the README.
|
|
57
|
+
const environment = await sm.environments.create({
|
|
58
|
+
name: 'quickstart',
|
|
59
|
+
// The default Salesforce app; sm.catalog.get() lists every app.
|
|
60
|
+
apps: [{ key: 'crm', appId: 'salesforce' }],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await sm.environments.waitUntilReady(environment.id);
|
|
65
|
+
|
|
66
|
+
// Salesforce has several actors (its users) and API profiles, so pick one of each.
|
|
67
|
+
// The connection lists the actors; this acts as the first.
|
|
68
|
+
const { actors } = await sm.environments.getConnection(environment.id, 'crm');
|
|
69
|
+
const [actor] = actors;
|
|
70
|
+
|
|
71
|
+
if (!actor) {
|
|
72
|
+
throw new Error('The Salesforce app has no actors.');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const crm = await sm.environments.connect(environment.id, 'crm', {
|
|
76
|
+
actorId: actor.id,
|
|
77
|
+
apiId: 'rest',
|
|
78
|
+
});
|
|
79
|
+
const response = await crm.fetch('/services/data/v67.0/limits');
|
|
80
|
+
|
|
81
|
+
console.log(response.status, await response.json());
|
|
82
|
+
} finally {
|
|
83
|
+
// Stop even when a step fails; lifetime counts from the start.
|
|
84
|
+
await sm.environments.stop(environment.id);
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Docs
|
|
89
|
+
|
|
90
|
+
- [npm package](https://www.npmjs.com/package/@usestatemachines/sdk): install, README and versions
|
|
91
|
+
- [Agent skill](https://state-web.usestatemachines.workers.dev/skill.md): workflow, rules and troubleshooting for coding agents
|
|
92
|
+
- [OpenAPI document](https://cdn.jsdelivr.net/npm/@usestatemachines/sdk/openapi.json): the HTTP API the SDK calls
|
|
93
|
+
- [State Machines](https://usestatemachines.com): product site
|