@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.
Files changed (203) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +311 -0
  3. package/agents/SKILL.md +90 -0
  4. package/agents/index.d.ts +13 -0
  5. package/agents/index.js +22 -0
  6. package/agents/llms.txt +93 -0
  7. package/agents/prompt.md +49 -0
  8. package/esm/core.d.ts +9 -0
  9. package/esm/core.js +12 -0
  10. package/esm/extensions/client.d.ts +81 -0
  11. package/esm/extensions/client.js +213 -0
  12. package/esm/extensions/connection.d.ts +42 -0
  13. package/esm/extensions/connection.js +133 -0
  14. package/esm/extensions/errors.d.ts +52 -0
  15. package/esm/extensions/errors.js +129 -0
  16. package/esm/extensions/index.d.ts +11 -0
  17. package/esm/extensions/index.js +7 -0
  18. package/esm/extensions/signal.d.ts +14 -0
  19. package/esm/extensions/signal.js +29 -0
  20. package/esm/extensions/wait.d.ts +19 -0
  21. package/esm/extensions/wait.js +102 -0
  22. package/esm/funcs/catalog-get.d.ts +11 -0
  23. package/esm/funcs/catalog-get.js +76 -0
  24. package/esm/funcs/catalog-release.d.ts +11 -0
  25. package/esm/funcs/catalog-release.js +92 -0
  26. package/esm/funcs/credential-get.d.ts +11 -0
  27. package/esm/funcs/credential-get.js +76 -0
  28. package/esm/funcs/environments-create.d.ts +11 -0
  29. package/esm/funcs/environments-create.js +94 -0
  30. package/esm/funcs/environments-events.d.ts +12 -0
  31. package/esm/funcs/environments-events.js +98 -0
  32. package/esm/funcs/environments-get-connection.d.ts +11 -0
  33. package/esm/funcs/environments-get-connection.js +96 -0
  34. package/esm/funcs/environments-get.d.ts +11 -0
  35. package/esm/funcs/environments-get.js +92 -0
  36. package/esm/funcs/environments-list.d.ts +11 -0
  37. package/esm/funcs/environments-list.js +100 -0
  38. package/esm/funcs/environments-reveal.d.ts +11 -0
  39. package/esm/funcs/environments-reveal.js +97 -0
  40. package/esm/funcs/environments-stop.d.ts +11 -0
  41. package/esm/funcs/environments-stop.js +92 -0
  42. package/esm/funcs/workspaces-events.d.ts +12 -0
  43. package/esm/funcs/workspaces-events.js +98 -0
  44. package/esm/funcs/workspaces-get.d.ts +11 -0
  45. package/esm/funcs/workspaces-get.js +92 -0
  46. package/esm/funcs/workspaces-list.d.ts +11 -0
  47. package/esm/funcs/workspaces-list.js +90 -0
  48. package/esm/hooks/hooks.d.ts +24 -0
  49. package/esm/hooks/hooks.js +79 -0
  50. package/esm/hooks/index.d.ts +2 -0
  51. package/esm/hooks/index.js +5 -0
  52. package/esm/hooks/types.d.ts +76 -0
  53. package/esm/hooks/types.js +4 -0
  54. package/esm/index.d.ts +5 -0
  55. package/esm/index.js +7 -0
  56. package/esm/lib/base64.d.ts +9 -0
  57. package/esm/lib/base64.js +30 -0
  58. package/esm/lib/config.d.ts +37 -0
  59. package/esm/lib/config.js +30 -0
  60. package/esm/lib/encodings.d.ts +54 -0
  61. package/esm/lib/encodings.js +406 -0
  62. package/esm/lib/files.d.ts +25 -0
  63. package/esm/lib/files.js +90 -0
  64. package/esm/lib/http.d.ts +66 -0
  65. package/esm/lib/http.js +208 -0
  66. package/esm/lib/logger.d.ts +5 -0
  67. package/esm/lib/logger.js +4 -0
  68. package/esm/lib/matchers.d.ts +58 -0
  69. package/esm/lib/matchers.js +201 -0
  70. package/esm/lib/primitives.d.ts +26 -0
  71. package/esm/lib/primitives.js +119 -0
  72. package/esm/lib/retries.d.ts +37 -0
  73. package/esm/lib/retries.js +156 -0
  74. package/esm/lib/schemas.d.ts +20 -0
  75. package/esm/lib/schemas.js +56 -0
  76. package/esm/lib/sdks.d.ts +62 -0
  77. package/esm/lib/sdks.js +289 -0
  78. package/esm/lib/security.d.ts +83 -0
  79. package/esm/lib/security.js +145 -0
  80. package/esm/lib/url.d.ts +4 -0
  81. package/esm/lib/url.js +23 -0
  82. package/esm/models/actor.d.ts +12 -0
  83. package/esm/models/actor.js +16 -0
  84. package/esm/models/api-profile.d.ts +17 -0
  85. package/esm/models/api-profile.js +22 -0
  86. package/esm/models/app.d.ts +14 -0
  87. package/esm/models/app.js +18 -0
  88. package/esm/models/catalog.d.ts +12 -0
  89. package/esm/models/catalog.js +15 -0
  90. package/esm/models/connection-secret.d.ts +14 -0
  91. package/esm/models/connection-secret.js +18 -0
  92. package/esm/models/connection.d.ts +26 -0
  93. package/esm/models/connection.js +30 -0
  94. package/esm/models/create-environment-input.d.ts +35 -0
  95. package/esm/models/create-environment-input.js +23 -0
  96. package/esm/models/credential-kind.d.ts +9 -0
  97. package/esm/models/credential-kind.js +10 -0
  98. package/esm/models/credential.d.ts +48 -0
  99. package/esm/models/credential.js +44 -0
  100. package/esm/models/environment-app.d.ts +42 -0
  101. package/esm/models/environment-app.js +44 -0
  102. package/esm/models/environment-lifetime.d.ts +10 -0
  103. package/esm/models/environment-lifetime.js +14 -0
  104. package/esm/models/environment.d.ts +47 -0
  105. package/esm/models/environment.js +48 -0
  106. package/esm/models/error-response.d.ts +45 -0
  107. package/esm/models/error-response.js +47 -0
  108. package/esm/models/errors/error-response.d.ts +18 -0
  109. package/esm/models/errors/error-response.js +32 -0
  110. package/esm/models/errors/http-client-errors.d.ts +43 -0
  111. package/esm/models/errors/http-client-errors.js +55 -0
  112. package/esm/models/errors/index.d.ts +6 -0
  113. package/esm/models/errors/index.js +9 -0
  114. package/esm/models/errors/response-validation-error.d.ts +25 -0
  115. package/esm/models/errors/response-validation-error.js +36 -0
  116. package/esm/models/errors/sdk-validation-error.d.ts +20 -0
  117. package/esm/models/errors/sdk-validation-error.js +51 -0
  118. package/esm/models/errors/state-machines-http-error.d.ts +18 -0
  119. package/esm/models/errors/state-machines-http-error.js +25 -0
  120. package/esm/models/errors/unexpected-response-error.d.ts +9 -0
  121. package/esm/models/errors/unexpected-response-error.js +29 -0
  122. package/esm/models/event-page.d.ts +11 -0
  123. package/esm/models/event-page.js +16 -0
  124. package/esm/models/event.d.ts +20 -0
  125. package/esm/models/event.js +22 -0
  126. package/esm/models/index.d.ts +19 -0
  127. package/esm/models/index.js +22 -0
  128. package/esm/models/operations/create-environments.d.ts +27 -0
  129. package/esm/models/operations/create-environments.js +27 -0
  130. package/esm/models/operations/get-catalog.d.ts +13 -0
  131. package/esm/models/operations/get-catalog.js +13 -0
  132. package/esm/models/operations/get-connection.d.ts +25 -0
  133. package/esm/models/operations/get-connection.js +21 -0
  134. package/esm/models/operations/get-credential.d.ts +13 -0
  135. package/esm/models/operations/get-credential.js +13 -0
  136. package/esm/models/operations/get-environment.d.ts +23 -0
  137. package/esm/models/operations/get-environment.js +20 -0
  138. package/esm/models/operations/get-release.d.ts +23 -0
  139. package/esm/models/operations/get-release.js +20 -0
  140. package/esm/models/operations/get-workspace.d.ts +23 -0
  141. package/esm/models/operations/get-workspace.js +20 -0
  142. package/esm/models/operations/index.d.ts +13 -0
  143. package/esm/models/operations/index.js +16 -0
  144. package/esm/models/operations/list-environment-events.d.ts +15 -0
  145. package/esm/models/operations/list-environment-events.js +13 -0
  146. package/esm/models/operations/list-environments.d.ts +49 -0
  147. package/esm/models/operations/list-environments.js +40 -0
  148. package/esm/models/operations/list-workspace-events.d.ts +15 -0
  149. package/esm/models/operations/list-workspace-events.js +13 -0
  150. package/esm/models/operations/list-workspaces.d.ts +32 -0
  151. package/esm/models/operations/list-workspaces.js +27 -0
  152. package/esm/models/operations/reveal-connection.d.ts +37 -0
  153. package/esm/models/operations/reveal-connection.js +29 -0
  154. package/esm/models/operations/stop-environment.d.ts +23 -0
  155. package/esm/models/operations/stop-environment.js +20 -0
  156. package/esm/models/release-actor.d.ts +11 -0
  157. package/esm/models/release-actor.js +15 -0
  158. package/esm/models/release.d.ts +26 -0
  159. package/esm/models/release.js +29 -0
  160. package/esm/models/security.d.ts +11 -0
  161. package/esm/models/security.js +11 -0
  162. package/esm/models/workspace.d.ts +16 -0
  163. package/esm/models/workspace.js +21 -0
  164. package/esm/sdk/catalog.d.ts +6 -0
  165. package/esm/sdk/catalog.js +15 -0
  166. package/esm/sdk/credential.d.ts +5 -0
  167. package/esm/sdk/credential.js +11 -0
  168. package/esm/sdk/environments.d.ts +12 -0
  169. package/esm/sdk/environments.js +35 -0
  170. package/esm/sdk/index.d.ts +1 -0
  171. package/esm/sdk/index.js +4 -0
  172. package/esm/sdk/sdk.d.ts +15 -0
  173. package/esm/sdk/sdk.js +26 -0
  174. package/esm/sdk/workspaces.d.ts +8 -0
  175. package/esm/sdk/workspaces.js +19 -0
  176. package/esm/types/async.d.ts +22 -0
  177. package/esm/types/async.js +27 -0
  178. package/esm/types/blobs.d.ts +3 -0
  179. package/esm/types/blobs.js +24 -0
  180. package/esm/types/const-date-time.d.ts +2 -0
  181. package/esm/types/const-date-time.js +9 -0
  182. package/esm/types/default-to-zero-value.d.ts +11 -0
  183. package/esm/types/default-to-zero-value.js +30 -0
  184. package/esm/types/enums.d.ts +8 -0
  185. package/esm/types/enums.js +26 -0
  186. package/esm/types/fp.d.ts +30 -0
  187. package/esm/types/fp.js +30 -0
  188. package/esm/types/index.d.ts +9 -0
  189. package/esm/types/index.js +9 -0
  190. package/esm/types/operations.d.ts +26 -0
  191. package/esm/types/operations.js +76 -0
  192. package/esm/types/primitives.d.ts +8 -0
  193. package/esm/types/primitives.js +108 -0
  194. package/esm/types/rfcdate.d.ts +20 -0
  195. package/esm/types/rfcdate.js +42 -0
  196. package/esm/types/smart-union.d.ts +6 -0
  197. package/esm/types/smart-union.js +116 -0
  198. package/esm/types/streams.d.ts +1 -0
  199. package/esm/types/streams.js +14 -0
  200. package/esm/types/unrecognized.d.ts +15 -0
  201. package/esm/types/unrecognized.js +30 -0
  202. package/openapi.json +2853 -0
  203. 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
@@ -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
+ >;
@@ -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
+ });
@@ -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