@usestatemachines/sdk 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +175 -75
- package/agents/SKILL.md +24 -44
- package/agents/index.js +7 -7
- package/agents/llms.txt +18 -39
- package/agents/prompt.md +11 -34
- package/esm/extensions/apps.d.ts +8 -0
- package/esm/extensions/apps.js +23 -0
- package/esm/extensions/client.d.ts +33 -3
- package/esm/extensions/client.js +144 -28
- package/esm/extensions/errors.d.ts +24 -5
- package/esm/extensions/errors.js +3 -2
- package/esm/extensions/index.d.ts +3 -1
- package/esm/extensions/index.js +1 -0
- package/esm/extensions/running-environment.d.ts +29 -0
- package/esm/extensions/running-environment.js +35 -0
- package/esm/extensions/signal.d.ts +1 -2
- package/esm/extensions/signal.js +9 -4
- package/esm/extensions/wait.d.ts +1 -0
- package/esm/extensions/wait.js +1 -1
- package/esm/funcs/create-environment-definition.d.ts +11 -0
- package/esm/funcs/create-environment-definition.js +94 -0
- package/esm/lib/config.d.ts +2 -2
- package/esm/lib/config.js +2 -2
- package/esm/models/connection-secret.d.ts +16 -0
- package/esm/models/connection-secret.js +13 -0
- package/esm/models/connection.d.ts +14 -0
- package/esm/models/connection.js +14 -0
- package/esm/models/error-response.d.ts +4 -0
- package/esm/models/error-response.js +4 -0
- package/esm/models/operations/create-environment-definition.d.ts +143 -0
- package/esm/models/operations/create-environment-definition.js +90 -0
- package/esm/models/operations/index.d.ts +1 -0
- package/esm/models/operations/index.js +1 -0
- package/esm/models/operations/reveal-connection.d.ts +26 -4
- package/esm/models/operations/reveal-connection.js +28 -2
- package/esm/sdk/sdk.d.ts +3 -1
- package/esm/sdk/sdk.js +5 -0
- package/openapi.json +394 -7
- package/package.json +1 -1
package/agents/index.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
// Generated by scripts/build-agents.mjs from templates/ and examples/quickstart.ts.
|
|
2
2
|
|
|
3
3
|
export const prompt =
|
|
4
|
-
"Set up State Machines so this project can run code against a disposable replica of a SaaS app, such as Salesforce. Follow these steps in order.\n\n1. Install the SDK: `npm install @usestatemachines/sdk`. It is ESM only and runs on Node.js 20.20 or later and on Cloudflare Workers, not in browsers.\n2. Check that `STATE_API_KEY` is set without printing it: `test -n \"$STATE_API_KEY\"`. If it is missing, stop and ask me to set it; the SDK doesn't read `.env` files.
|
|
4
|
+
"Set up State Machines so this project can run code against a disposable replica of a SaaS app, such as Salesforce. Follow these steps in order.\n\n1. Install the SDK: `npm install @usestatemachines/sdk`. It is ESM only and runs on Node.js 20.20 or later and on Cloudflare Workers, not in browsers.\n2. Check that `STATE_API_KEY` is set without printing it: `test -n \"$STATE_API_KEY\"`. If it is missing, stop and ask me to set it; the SDK doesn't read `.env` files. In Cloudflare Workers, pass the key as `new StateMachines({ apiKey })`.\n3. Save this quickstart unchanged as `state-quickstart.mts` and run `npx tsx state-quickstart.mts` to confirm the setup. It prints `200` and the org's limits:\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATE_API_KEY\nconst env = await sm.environments.start({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce', { apiId: 'rest' });\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.stop();\n}\n```\n\n4. Adapt it to my task. Log `await sm.catalog.get()` to find each app's ID and the actors and API profiles its release offers; don't guess IDs. Start only the apps the task needs; a string such as `'salesforce'` means `{ key: 'salesforce', appId: 'salesforce' }`. Salesforce always needs an `apiId`. `connect()` acts as the declared default connection's actor, or else the app's first actor, the administrator; to act as a specific user, pass an `actorId` from `await env.actors('salesforce')`.\n5. Always call `env.stop()` in a `finally`. In CI, then await `env.waitUntilStopped()` so the run fails if cleanup isn't confirmed. If `start()` fails after creating the environment, it stops it; an `EnvironmentFailedError` or `TimeoutError` carries `err.environmentId` and `err.stopped`. If the create itself fails, call `sm.environments.start(input, { idempotencyKey: err.idempotencyKey })` with the same `input`; never retry without the key, which can start a second environment.\n6. Never print, log or commit keys or tokens, including `connection.headers()` and `connection.actorToken()`, which return live tokens.\n\nRead more: `node_modules/@usestatemachines/sdk/README.md`, https://state-web.usestatemachines.workers.dev/llms.txt and https://www.npmjs.com/package/@usestatemachines/sdk\n";
|
|
5
5
|
|
|
6
6
|
export const llmsTxt =
|
|
7
|
-
"# State Machines\n\n> State Machines runs disposable environments of replica SaaS apps, such as Salesforce, for tests and coding agents.
|
|
7
|
+
"# State Machines\n\n> State Machines runs disposable environments of replica SaaS apps, such as Salesforce, for tests and coding agents. Start, connect to and stop them with the TypeScript SDK `@usestatemachines/sdk`.\n\nInstall with `npm install @usestatemachines/sdk`. It is ESM only and runs on Node.js 20.20 or later (22 or 24 recommended) and Cloudflare Workers, not browsers. TypeScript needs `lib: [\"DOM\"]` or `@types/node`. `new StateMachines()` reads the API key from `STATE_API_KEY` and doesn't read `.env` files; in Cloudflare Workers, pass `apiKey` explicitly. Never print the key. The installed `node_modules/@usestatemachines/sdk/README.md` matches your version: read it before relying on memory.\n\n## Concepts\n\n- Environment: one disposable run of one or more apps. `const env = await sm.environments.start({ apps: ['salesforce'] })` creates it, waits until it is `running` and returns a `RunningEnvironment`. If the environment fails or the wait times out or is aborted, `start()` stops the environment and rethrows the original error; an `EnvironmentFailedError` or `TimeoutError` carries `err.environmentId` and `err.stopped`. Always call `env.stop()` in a `finally`.\n- Apps: a string is shorthand for `{ key: s, appId: s }`, nothing more. Use the object form, such as `{ key: 'crm', appId: 'salesforce' }`, for your own key or two instances of one app. `appId` names the app in the catalog (`sm.catalog.get()`); `key` is your name for it in this environment, and later calls take the key.\n- Actor: one of the app's users; requests run with that user's permissions. Salesforce actor IDs are its user IDs, such as `005…`, never a role name. `await env.actors('salesforce')` lists them in the release's order. connect() acts as the environment's declared default connection's actor, or else the first actor, the administrator, unless you pass `actorId`; pick a less-privileged actor to test permissions.\n- API profile: one API of the app, with its path and versions. Salesforce offers several (`rest`, `soap-partner`, `bulk2`, `oauth`) and has no default, so always pass `apiId`: `env.connect('salesforce', { apiId: 'rest' })`.\n- Connection: `connection.fetch` never sends your API key, never follows redirects and never retries. `connection.headers()` returns live tokens: treat them as secrets. Salesforce's SOAP profile (`soap-partner`) takes the actor token in the envelope instead: put `connection.actorToken()`, also a secret, in `SessionHeader/sessionId`; `headers()` then carries only `X-State-Environment-Token`.\n- Full control: `sm.environments.create(input)` returns one `Environment` before the apps boot, `sm.environments.waitUntilReady(environment.id)` waits, and `sm.environments.connect(environment.id, key, options)` connects. Nothing on this path stops the environment for you; use it to keep a failed environment for inspection.\n- 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- `start()` and `waitUntilReady` throw `EnvironmentFailedError` if the environment becomes `failed`, `stopping` or `stopped`, and `TimeoutError` after 10 minutes by default (`readyTimeoutMs` for `start()`, `timeoutMs` for `waitUntilReady`). `start()` then stops the environment; `waitUntilReady` never does.\n- `waitUntilStopped` resolves when the environment is `stopped` or `failed` and every app's disposal is `complete`. It throws `DisposalIncompleteError` if a disposal is `failed` or `unknown`, and `TimeoutError` if disposal stays `pending`.\n\n## Limits\n\n- Live environments are capped per organization: `(await sm.credential.get()).limits.liveEnvironments`.\n- `lifetimeMinutes` defaults to 15 minutes, or the deployment maximum if that is lower. Both are in `(await sm.credential.get()).limits.environmentLifetime`. Lifetime counts from the start, so boot uses part of it; at the end the environment stops with `stopReason: 'expired'`.\n- Always call `env.stop()` in a `finally`. In CI, then await `env.waitUntilStopped()`.\n\n## Errors and retries\n\nA failed API call throws `StateMachinesError` with `status`, `code`, `requestId` and `retryAfter`. The SDK retries `502`, `503`, `504`, connection errors and attempt timeouts itself, within a 10-minute retry budget (the last attempt can end later); `create` retries reuse one idempotency key. It retries nothing else: not `429`, other `4xx` or other `5xx`. `err.stopped === false` with `err.stopError` means the stop wasn't confirmed; `sm.environments.stop(err.environmentId)` is safe to repeat. Without `stopError`, you passed your own `idempotencyKey`, so `start()` left the environment running because another `start()` with that key may be using it; stop it only once nothing else needs it. Every error from `create`, including the create step of `start()`, carries `err.idempotencyKey`: `sm.environments.start(input, { idempotencyKey: err.idempotencyKey })` with the same input returns the environment the first call created. Invalid `apps` fail before any request as a `StateMachinesError` (`status` null, `cause` a `TypeError`; don't replay it) with one of: `Pass at least one app.`, `App key \"<key>\" is used twice.` or `\"<s>\" can't be used as an app key; pass { key, appId } instead.`\n\n| Code | Status | What to do |\n| --- | --- | --- |\n| `environment-limit` | 429 | Not retried. Stop a live environment (`sm.environments.list({ status: 'live' })`), then start again. |\n| `lifetime-too-long` | 422 | Lower `lifetimeMinutes` to at most `limits.environmentLifetime.maximumMinutes`. |\n| `release-unavailable` | 422 | Unknown or retired app or release. Check `appId` and `appReleaseId` against `sm.catalog.get()`. |\n| `release-unavailable` | 503 | This deployment doesn't serve the release. Retried within the budget; omit `appReleaseId` to use the default release, or contact support with the `requestId`. A failed app can carry it as `errorCode`. |\n| `connection-unavailable` | 409 | The app stopped or the environment is no longer `running`. If it stopped or expired, start a new one. (Before revealing any token, connect() throws a `TypeError` for an app that isn't ready, an unknown actor, or a missing `apiId`.) |\n| `idempotency-conflict` | 409 | The idempotency key was used with a different input. Replay with the exact same input. |\n| `request-timeout` | 503 | Retried automatically. If `create` still fails, replay it with `err.idempotencyKey`. |\n\n## Quickstart\n\nSave it unchanged as `state-quickstart.mts` and run `npx tsx state-quickstart.mts`.\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATE_API_KEY\nconst env = await sm.environments.start({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce', { apiId: 'rest' });\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.stop();\n}\n```\n\n## Docs\n\n- [npm package](https://www.npmjs.com/package/@usestatemachines/sdk): install, README and versions\n- [Agent skill](https://state-web.usestatemachines.workers.dev/skill.md): workflow, rules and troubleshooting for coding agents\n- [OpenAPI document](https://cdn.jsdelivr.net/npm/@usestatemachines/sdk/openapi.json): the HTTP API the SDK calls\n- [State Machines](https://usestatemachines.com): product site\n";
|
|
8
8
|
|
|
9
9
|
export const skill =
|
|
10
|
-
"---\nname: state-machines\ndescription: Use when an agent or test needs a disposable replica of a SaaS app such as Salesforce, to run integration tests or try API calls against a sandboxed copy instead of a real org. Covers the @usestatemachines/sdk TypeScript SDK -
|
|
10
|
+
"---\nname: state-machines\ndescription: Use when an agent or test needs a disposable replica of a SaaS app such as Salesforce, to run integration tests or try API calls against a sandboxed copy instead of a real org. Covers the @usestatemachines/sdk TypeScript SDK - starting environments, choosing an actor, cleanup, and errors such as environment-limit and release-unavailable. Also use when the user mentions State Machines, STATE_API_KEY or @usestatemachines/sdk.\n---\n\nRead `node_modules/@usestatemachines/sdk/README.md` before relying on memory; it matches the installed version. If the package is missing, run `npm install @usestatemachines/sdk`.\n\n## Workflow\n\n1. Check that `STATE_API_KEY` is set without printing it (`test -n \"$STATE_API_KEY\"`). If it isn't, ask the user to set it; the SDK doesn't read `.env` files.\n2. Start the environment with only the apps the task needs: `const env = await sm.environments.start({ apps: ['salesforce'] })`. Log `await sm.catalog.get()` for each app's ID and the actors and API profiles its release offers; don't guess IDs. A string app means `{ key: s, appId: s }`; use `{ key, appId }` for your own key. `start()` waits until the environment is `running`. If that fails, it stops the environment and rethrows; an `EnvironmentFailedError` or `TimeoutError` carries `err.environmentId` and `err.stopped`.\n3. Open a `try` right after `start()` returns, and call `env.stop()` in its `finally`.\n4. Connect through one API profile. Salesforce offers several and has no default, so pass `apiId`: `env.connect('salesforce', { apiId: 'rest' })`. This acts as the environment's declared default connection's actor, or else the app's first actor, the administrator. To act with another user's permissions, choose one from `await env.actors('salesforce')` and pass its `id` as `actorId`. Salesforce actor IDs are its user IDs, such as `005…`, never a role name, so read them instead of hard-coding one.\n5. Call the app with the connection's `fetch`. A leading `/` means the root of `baseUrl`.\n6. In `finally`, `await env.stop()`. In CI, then `await env.waitUntilStopped()`.\n\nThe whole flow, as the package ships it. It is ESM with top-level `await`: save it as `state-quickstart.mts` and run `npx tsx state-quickstart.mts`.\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATE_API_KEY\nconst env = await sm.environments.start({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce', { apiId: 'rest' });\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.stop();\n}\n```\n\n## Rules\n\n- Always stop in `finally`. `waitUntilStopped` confirms every app was disposed; use it in CI so a run fails when cleanup can't be confirmed.\n- To keep a failed environment for inspection, use the full-control path: `sm.environments.create`, then `sm.environments.waitUntilReady(environment.id)`, then `sm.environments.connect(environment.id, key, options)`. Nothing on that path stops the environment for you.\n- Lifetime counts from the start, including boot. It defaults to 15 minutes, or the deployment maximum if that is lower: `(await sm.credential.get()).limits.environmentLifetime`. Set `lifetimeMinutes` in `start` or `create` for longer runs.\n- Live environments are capped per organization: `limits.liveEnvironments` from `sm.credential.get()`. Stop the ones you no longer need.\n- Never print, log or commit `STATE_API_KEY`, tokens, or `headers()` or `actorToken()` output.\n- Salesforce's SOAP profile (`soap-partner`) takes the actor token in the envelope, not a header: put `connection.actorToken()` in `SessionHeader/sessionId`. For it, `headers()` carries only `X-State-Environment-Token`.\n- `connection.fetch` never sends your API key, never follows redirects and only reaches URLs under `baseUrl`.\n- Runtimes: Node.js 20.20 or later (22 or 24 recommended), and Cloudflare Workers. ESM only. TypeScript needs `lib: [\"DOM\"]` or `@types/node`. Never ship an API key to a browser.\n- In Cloudflare Workers, pass the key explicitly: `new StateMachines({ apiKey: env.STATE_API_KEY })`, and bound calls with `{ signal: AbortSignal.timeout(20_000) }`, since retries can take minutes.\n- `environments.list`, `environments.events` 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 `start()` or `create` still throws with `err.idempotencyKey`, the create failed: don't call it again as a new request, which can start a second environment. Call `sm.environments.start(input, { idempotencyKey: err.idempotencyKey })` with the same `input`. Don't replay when `err.status` is `null` and `err.cause` is a `TypeError`: the input was invalid and nothing was sent; fix the input instead. A key only recovers a failed create: don't reuse the key of a `start()` whose wait failed, because the replay returns that stopped environment. With your own key, `start()` doesn't stop the environment on failure, because another `start()` with that key may be using it: the error carries `err.environmentId`, `err.stopped` is `false` and there is no `err.stopError`. Stop it only once nothing else needs it. It returns the environment the first call created, or creates it if that call never arrived. A different input fails with `idempotency-conflict`. Keys are kept for a day.\n\n## Troubleshooting\n\n| Symptom | Cause | Fix |\n| --- | --- | --- |\n| `release-unavailable`, 422 | Unknown or retired `appId` or `appReleaseId`. A string app is used as the `appId`. | Check the IDs against `sm.catalog.get()`. Omit `appReleaseId` for the default release. |\n| `release-unavailable`, 503, or a failed app with that `errorCode` | This deployment doesn't serve the release. The SDK retries a 503 within a 10-minute budget. | Omit `appReleaseId`. If it persists, report the `requestId`. Pass a `signal` to bound the wait. |\n| `environment-limit`, 429 | The organization is at its live-environment cap. Not retried. | `sm.environments.list({ status: 'live' })`, stop one you no longer need, then start again. |\n| `lifetime-too-long`, 422 | `lifetimeMinutes` is above the maximum. | Use at most `limits.environmentLifetime.maximumMinutes`. |\n| `TypeError: Unknown actor <id> for <appKey>. Choose one of: <ids>.` | The `actorId` isn't one of the app's actors. No token was revealed. | Pass an `id` from `await env.actors(appKey)`, or omit `actorId` to act as the default actor. |\n| `TypeError: Choose one API for <appKey>: <ids>.` | The app has several API profiles and `apiId` is missing or unknown. Salesforce always needs one. | Pass `apiId` from the listed IDs, such as `rest`. |\n| `start()` rejects with `EnvironmentFailedError` or `TimeoutError` | The environment failed, or wasn't `running` within `readyTimeoutMs` (10 minutes by default). If `err.stopped` is `true`, `start()` already stopped it. | Read `err.environment` (`null` on a `TimeoutError` that saw no read): its `stopReason` and each app's `errorCode`. If `err.stopped` is `false`, read `err.stopError` and call `sm.environments.stop(err.environmentId)`. To keep a failed environment, use the full-control path. |\n| `StateMachinesError` (`status` null) for invalid `apps`: `Pass at least one app.`, `App key \"<key>\" is used twice.` or `\"<s>\" can't be used as an app key; pass { key, appId } instead.` | Empty list, duplicate key, or a string that isn't a valid key. No request was sent. | Pass one to ten apps with unique keys; use `{ key, appId }` when the ID isn't a valid key. |\n| `TypeError: App ... is not ready to connect` | On the full-control path, connect() ran before the app was `ready`. | Await `waitUntilReady` first, or use `start()`. |\n| `connection-unavailable`, 409 | The app stopped, or the environment is no longer `running`. | Check `sm.environments.get(env.id)`; if it stopped or expired, start a new one. |\n| `EnvironmentFailedError` from `waitUntilReady` | On the full-control path, the environment became `failed`, `stopping` or `stopped`. | Read `err.environment.stopReason` and each app's `errorCode`. Still stop it in `finally`. |\n| A vendor client such as jsforce is rejected by the gateway | The gateway needs `X-State-Environment-Token` on every request; jsforce only sends it with custom headers configured. | Prefer `connection.fetch`. If you pass `headers()` to another client, send them only to `baseUrl` and disable redirects. |\n| `TypeError: Pass apiKey or set STATE_API_KEY.` | No key in the environment. | Ask the user to set `STATE_API_KEY`; in Workers, pass `apiKey`. |\n\nMore: https://state-web.usestatemachines.workers.dev/llms.txt and https://www.npmjs.com/package/@usestatemachines/sdk\n";
|
|
11
11
|
|
|
12
12
|
export const quickstart =
|
|
13
|
-
"import { StateMachines } from '@usestatemachines/sdk';\n\
|
|
13
|
+
"import { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATE_API_KEY\nconst env = await sm.environments.start({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce', { apiId: 'rest' });\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.stop();\n}\n";
|
|
14
14
|
|
|
15
15
|
export const templates = Object.freeze({
|
|
16
16
|
prompt:
|
|
17
|
-
"Set up State Machines so this project can run code against a disposable replica of a SaaS app, such as Salesforce. Follow these steps in order.\n\n1. Install the SDK: `npm install @usestatemachines/sdk`. It is ESM only and runs on Node.js 20.20 or later and on Cloudflare Workers, not in browsers.\n2. Check that `STATE_API_KEY` is set without printing it: `test -n \"$STATE_API_KEY\"`. If it is missing, stop and ask me to set it; the SDK doesn't read `.env` files.
|
|
17
|
+
"Set up State Machines so this project can run code against a disposable replica of a SaaS app, such as Salesforce. Follow these steps in order.\n\n1. Install the SDK: `npm install @usestatemachines/sdk`. It is ESM only and runs on Node.js 20.20 or later and on Cloudflare Workers, not in browsers.\n2. Check that `STATE_API_KEY` is set without printing it: `test -n \"$STATE_API_KEY\"`. If it is missing, stop and ask me to set it; the SDK doesn't read `.env` files. In Cloudflare Workers, pass the key as `new StateMachines({ apiKey })`.\n3. Save this quickstart unchanged as `state-quickstart.mts` and run `npx tsx state-quickstart.mts` to confirm the setup. It prints `200` and the org's limits:\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATE_API_KEY\nconst env = await sm.environments.start({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce', { apiId: 'rest' });\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.stop();\n}\n```\n\n4. Adapt it to my task. Log `await sm.catalog.get()` to find each app's ID and the actors and API profiles its release offers; don't guess IDs. Start only the apps the task needs; a string such as `'salesforce'` means `{ key: 'salesforce', appId: 'salesforce' }`. Salesforce always needs an `apiId`. `connect()` acts as the declared default connection's actor, or else the app's first actor, the administrator; to act as a specific user, pass an `actorId` from `await env.actors('salesforce')`.\n5. Always call `env.stop()` in a `finally`. In CI, then await `env.waitUntilStopped()` so the run fails if cleanup isn't confirmed. If `start()` fails after creating the environment, it stops it; an `EnvironmentFailedError` or `TimeoutError` carries `err.environmentId` and `err.stopped`. If the create itself fails, call `sm.environments.start(input, { idempotencyKey: err.idempotencyKey })` with the same `input`; never retry without the key, which can start a second environment.\n6. Never print, log or commit keys or tokens, including `connection.headers()` and `connection.actorToken()`, which return live tokens.\n\nRead more: `node_modules/@usestatemachines/sdk/README.md`, {{origin}}/llms.txt and https://www.npmjs.com/package/@usestatemachines/sdk\n",
|
|
18
18
|
llmsTxt:
|
|
19
|
-
"# State Machines\n\n> State Machines runs disposable environments of replica SaaS apps, such as Salesforce, for tests and coding agents.
|
|
19
|
+
"# State Machines\n\n> State Machines runs disposable environments of replica SaaS apps, such as Salesforce, for tests and coding agents. Start, connect to and stop them with the TypeScript SDK `@usestatemachines/sdk`.\n\nInstall with `npm install @usestatemachines/sdk`. It is ESM only and runs on Node.js 20.20 or later (22 or 24 recommended) and Cloudflare Workers, not browsers. TypeScript needs `lib: [\"DOM\"]` or `@types/node`. `new StateMachines()` reads the API key from `STATE_API_KEY` and doesn't read `.env` files; in Cloudflare Workers, pass `apiKey` explicitly. Never print the key. The installed `node_modules/@usestatemachines/sdk/README.md` matches your version: read it before relying on memory.\n\n## Concepts\n\n- Environment: one disposable run of one or more apps. `const env = await sm.environments.start({ apps: ['salesforce'] })` creates it, waits until it is `running` and returns a `RunningEnvironment`. If the environment fails or the wait times out or is aborted, `start()` stops the environment and rethrows the original error; an `EnvironmentFailedError` or `TimeoutError` carries `err.environmentId` and `err.stopped`. Always call `env.stop()` in a `finally`.\n- Apps: a string is shorthand for `{ key: s, appId: s }`, nothing more. Use the object form, such as `{ key: 'crm', appId: 'salesforce' }`, for your own key or two instances of one app. `appId` names the app in the catalog (`sm.catalog.get()`); `key` is your name for it in this environment, and later calls take the key.\n- Actor: one of the app's users; requests run with that user's permissions. Salesforce actor IDs are its user IDs, such as `005…`, never a role name. `await env.actors('salesforce')` lists them in the release's order. connect() acts as the environment's declared default connection's actor, or else the first actor, the administrator, unless you pass `actorId`; pick a less-privileged actor to test permissions.\n- API profile: one API of the app, with its path and versions. Salesforce offers several (`rest`, `soap-partner`, `bulk2`, `oauth`) and has no default, so always pass `apiId`: `env.connect('salesforce', { apiId: 'rest' })`.\n- Connection: `connection.fetch` never sends your API key, never follows redirects and never retries. `connection.headers()` returns live tokens: treat them as secrets. Salesforce's SOAP profile (`soap-partner`) takes the actor token in the envelope instead: put `connection.actorToken()`, also a secret, in `SessionHeader/sessionId`; `headers()` then carries only `X-State-Environment-Token`.\n- Full control: `sm.environments.create(input)` returns one `Environment` before the apps boot, `sm.environments.waitUntilReady(environment.id)` waits, and `sm.environments.connect(environment.id, key, options)` connects. Nothing on this path stops the environment for you; use it to keep a failed environment for inspection.\n- 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- `start()` and `waitUntilReady` throw `EnvironmentFailedError` if the environment becomes `failed`, `stopping` or `stopped`, and `TimeoutError` after 10 minutes by default (`readyTimeoutMs` for `start()`, `timeoutMs` for `waitUntilReady`). `start()` then stops the environment; `waitUntilReady` never does.\n- `waitUntilStopped` resolves when the environment is `stopped` or `failed` and every app's disposal is `complete`. It throws `DisposalIncompleteError` if a disposal is `failed` or `unknown`, and `TimeoutError` if disposal stays `pending`.\n\n## Limits\n\n- Live environments are capped per organization: `(await sm.credential.get()).limits.liveEnvironments`.\n- `lifetimeMinutes` defaults to 15 minutes, or the deployment maximum if that is lower. Both are in `(await sm.credential.get()).limits.environmentLifetime`. Lifetime counts from the start, so boot uses part of it; at the end the environment stops with `stopReason: 'expired'`.\n- Always call `env.stop()` in a `finally`. In CI, then await `env.waitUntilStopped()`.\n\n## Errors and retries\n\nA failed API call throws `StateMachinesError` with `status`, `code`, `requestId` and `retryAfter`. The SDK retries `502`, `503`, `504`, connection errors and attempt timeouts itself, within a 10-minute retry budget (the last attempt can end later); `create` retries reuse one idempotency key. It retries nothing else: not `429`, other `4xx` or other `5xx`. `err.stopped === false` with `err.stopError` means the stop wasn't confirmed; `sm.environments.stop(err.environmentId)` is safe to repeat. Without `stopError`, you passed your own `idempotencyKey`, so `start()` left the environment running because another `start()` with that key may be using it; stop it only once nothing else needs it. Every error from `create`, including the create step of `start()`, carries `err.idempotencyKey`: `sm.environments.start(input, { idempotencyKey: err.idempotencyKey })` with the same input returns the environment the first call created. Invalid `apps` fail before any request as a `StateMachinesError` (`status` null, `cause` a `TypeError`; don't replay it) with one of: `Pass at least one app.`, `App key \"<key>\" is used twice.` or `\"<s>\" can't be used as an app key; pass { key, appId } instead.`\n\n| Code | Status | What to do |\n| --- | --- | --- |\n| `environment-limit` | 429 | Not retried. Stop a live environment (`sm.environments.list({ status: 'live' })`), then start again. |\n| `lifetime-too-long` | 422 | Lower `lifetimeMinutes` to at most `limits.environmentLifetime.maximumMinutes`. |\n| `release-unavailable` | 422 | Unknown or retired app or release. Check `appId` and `appReleaseId` against `sm.catalog.get()`. |\n| `release-unavailable` | 503 | This deployment doesn't serve the release. Retried within the budget; omit `appReleaseId` to use the default release, or contact support with the `requestId`. A failed app can carry it as `errorCode`. |\n| `connection-unavailable` | 409 | The app stopped or the environment is no longer `running`. If it stopped or expired, start a new one. (Before revealing any token, connect() throws a `TypeError` for an app that isn't ready, an unknown actor, or a missing `apiId`.) |\n| `idempotency-conflict` | 409 | The idempotency key was used with a different input. Replay with the exact same input. |\n| `request-timeout` | 503 | Retried automatically. If `create` still fails, replay it with `err.idempotencyKey`. |\n\n## Quickstart\n\nSave it unchanged as `state-quickstart.mts` and run `npx tsx state-quickstart.mts`.\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATE_API_KEY\nconst env = await sm.environments.start({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce', { apiId: 'rest' });\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.stop();\n}\n```\n\n## Docs\n\n- [npm package](https://www.npmjs.com/package/@usestatemachines/sdk): install, README and versions\n- [Agent skill]({{origin}}/skill.md): workflow, rules and troubleshooting for coding agents\n- [OpenAPI document](https://cdn.jsdelivr.net/npm/@usestatemachines/sdk/openapi.json): the HTTP API the SDK calls\n- [State Machines](https://usestatemachines.com): product site\n",
|
|
20
20
|
skill:
|
|
21
|
-
"---\nname: state-machines\ndescription: Use when an agent or test needs a disposable replica of a SaaS app such as Salesforce, to run integration tests or try API calls against a sandboxed copy instead of a real org. Covers the @usestatemachines/sdk TypeScript SDK -
|
|
21
|
+
"---\nname: state-machines\ndescription: Use when an agent or test needs a disposable replica of a SaaS app such as Salesforce, to run integration tests or try API calls against a sandboxed copy instead of a real org. Covers the @usestatemachines/sdk TypeScript SDK - starting environments, choosing an actor, cleanup, and errors such as environment-limit and release-unavailable. Also use when the user mentions State Machines, STATE_API_KEY or @usestatemachines/sdk.\n---\n\nRead `node_modules/@usestatemachines/sdk/README.md` before relying on memory; it matches the installed version. If the package is missing, run `npm install @usestatemachines/sdk`.\n\n## Workflow\n\n1. Check that `STATE_API_KEY` is set without printing it (`test -n \"$STATE_API_KEY\"`). If it isn't, ask the user to set it; the SDK doesn't read `.env` files.\n2. Start the environment with only the apps the task needs: `const env = await sm.environments.start({ apps: ['salesforce'] })`. Log `await sm.catalog.get()` for each app's ID and the actors and API profiles its release offers; don't guess IDs. A string app means `{ key: s, appId: s }`; use `{ key, appId }` for your own key. `start()` waits until the environment is `running`. If that fails, it stops the environment and rethrows; an `EnvironmentFailedError` or `TimeoutError` carries `err.environmentId` and `err.stopped`.\n3. Open a `try` right after `start()` returns, and call `env.stop()` in its `finally`.\n4. Connect through one API profile. Salesforce offers several and has no default, so pass `apiId`: `env.connect('salesforce', { apiId: 'rest' })`. This acts as the environment's declared default connection's actor, or else the app's first actor, the administrator. To act with another user's permissions, choose one from `await env.actors('salesforce')` and pass its `id` as `actorId`. Salesforce actor IDs are its user IDs, such as `005…`, never a role name, so read them instead of hard-coding one.\n5. Call the app with the connection's `fetch`. A leading `/` means the root of `baseUrl`.\n6. In `finally`, `await env.stop()`. In CI, then `await env.waitUntilStopped()`.\n\nThe whole flow, as the package ships it. It is ESM with top-level `await`: save it as `state-quickstart.mts` and run `npx tsx state-quickstart.mts`.\n\n```ts\nimport { StateMachines } from '@usestatemachines/sdk';\n\nconst sm = new StateMachines(); // reads STATE_API_KEY\nconst env = await sm.environments.start({ apps: ['salesforce'] });\n\ntry {\n const salesforce = await env.connect('salesforce', { apiId: 'rest' });\n const res = await salesforce.fetch('/services/data/v67.0/limits');\n console.log(res.status, await res.json());\n} finally {\n await env.stop();\n}\n```\n\n## Rules\n\n- Always stop in `finally`. `waitUntilStopped` confirms every app was disposed; use it in CI so a run fails when cleanup can't be confirmed.\n- To keep a failed environment for inspection, use the full-control path: `sm.environments.create`, then `sm.environments.waitUntilReady(environment.id)`, then `sm.environments.connect(environment.id, key, options)`. Nothing on that path stops the environment for you.\n- Lifetime counts from the start, including boot. It defaults to 15 minutes, or the deployment maximum if that is lower: `(await sm.credential.get()).limits.environmentLifetime`. Set `lifetimeMinutes` in `start` or `create` for longer runs.\n- Live environments are capped per organization: `limits.liveEnvironments` from `sm.credential.get()`. Stop the ones you no longer need.\n- Never print, log or commit `STATE_API_KEY`, tokens, or `headers()` or `actorToken()` output.\n- Salesforce's SOAP profile (`soap-partner`) takes the actor token in the envelope, not a header: put `connection.actorToken()` in `SessionHeader/sessionId`. For it, `headers()` carries only `X-State-Environment-Token`.\n- `connection.fetch` never sends your API key, never follows redirects and only reaches URLs under `baseUrl`.\n- Runtimes: Node.js 20.20 or later (22 or 24 recommended), and Cloudflare Workers. ESM only. TypeScript needs `lib: [\"DOM\"]` or `@types/node`. Never ship an API key to a browser.\n- In Cloudflare Workers, pass the key explicitly: `new StateMachines({ apiKey: env.STATE_API_KEY })`, and bound calls with `{ signal: AbortSignal.timeout(20_000) }`, since retries can take minutes.\n- `environments.list`, `environments.events` 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 `start()` or `create` still throws with `err.idempotencyKey`, the create failed: don't call it again as a new request, which can start a second environment. Call `sm.environments.start(input, { idempotencyKey: err.idempotencyKey })` with the same `input`. Don't replay when `err.status` is `null` and `err.cause` is a `TypeError`: the input was invalid and nothing was sent; fix the input instead. A key only recovers a failed create: don't reuse the key of a `start()` whose wait failed, because the replay returns that stopped environment. With your own key, `start()` doesn't stop the environment on failure, because another `start()` with that key may be using it: the error carries `err.environmentId`, `err.stopped` is `false` and there is no `err.stopError`. Stop it only once nothing else needs it. It returns the environment the first call created, or creates it if that call never arrived. A different input fails with `idempotency-conflict`. Keys are kept for a day.\n\n## Troubleshooting\n\n| Symptom | Cause | Fix |\n| --- | --- | --- |\n| `release-unavailable`, 422 | Unknown or retired `appId` or `appReleaseId`. A string app is used as the `appId`. | Check the IDs against `sm.catalog.get()`. Omit `appReleaseId` for the default release. |\n| `release-unavailable`, 503, or a failed app with that `errorCode` | This deployment doesn't serve the release. The SDK retries a 503 within a 10-minute budget. | Omit `appReleaseId`. If it persists, report the `requestId`. Pass a `signal` to bound the wait. |\n| `environment-limit`, 429 | The organization is at its live-environment cap. Not retried. | `sm.environments.list({ status: 'live' })`, stop one you no longer need, then start again. |\n| `lifetime-too-long`, 422 | `lifetimeMinutes` is above the maximum. | Use at most `limits.environmentLifetime.maximumMinutes`. |\n| `TypeError: Unknown actor <id> for <appKey>. Choose one of: <ids>.` | The `actorId` isn't one of the app's actors. No token was revealed. | Pass an `id` from `await env.actors(appKey)`, or omit `actorId` to act as the default actor. |\n| `TypeError: Choose one API for <appKey>: <ids>.` | The app has several API profiles and `apiId` is missing or unknown. Salesforce always needs one. | Pass `apiId` from the listed IDs, such as `rest`. |\n| `start()` rejects with `EnvironmentFailedError` or `TimeoutError` | The environment failed, or wasn't `running` within `readyTimeoutMs` (10 minutes by default). If `err.stopped` is `true`, `start()` already stopped it. | Read `err.environment` (`null` on a `TimeoutError` that saw no read): its `stopReason` and each app's `errorCode`. If `err.stopped` is `false`, read `err.stopError` and call `sm.environments.stop(err.environmentId)`. To keep a failed environment, use the full-control path. |\n| `StateMachinesError` (`status` null) for invalid `apps`: `Pass at least one app.`, `App key \"<key>\" is used twice.` or `\"<s>\" can't be used as an app key; pass { key, appId } instead.` | Empty list, duplicate key, or a string that isn't a valid key. No request was sent. | Pass one to ten apps with unique keys; use `{ key, appId }` when the ID isn't a valid key. |\n| `TypeError: App ... is not ready to connect` | On the full-control path, connect() ran before the app was `ready`. | Await `waitUntilReady` first, or use `start()`. |\n| `connection-unavailable`, 409 | The app stopped, or the environment is no longer `running`. | Check `sm.environments.get(env.id)`; if it stopped or expired, start a new one. |\n| `EnvironmentFailedError` from `waitUntilReady` | On the full-control path, the environment became `failed`, `stopping` or `stopped`. | Read `err.environment.stopReason` and each app's `errorCode`. Still stop it in `finally`. |\n| A vendor client such as jsforce is rejected by the gateway | The gateway needs `X-State-Environment-Token` on every request; jsforce only sends it with custom headers configured. | Prefer `connection.fetch`. If you pass `headers()` to another client, send them only to `baseUrl` and disable redirects. |\n| `TypeError: Pass apiKey or set STATE_API_KEY.` | No key in the environment. | Ask the user to set `STATE_API_KEY`; in Workers, pass `apiKey`. |\n\nMore: {{origin}}/llms.txt and https://www.npmjs.com/package/@usestatemachines/sdk\n",
|
|
22
22
|
});
|
package/agents/llms.txt
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
# State Machines
|
|
2
2
|
|
|
3
|
-
> State Machines runs disposable environments of replica SaaS apps, such as Salesforce, for tests and coding agents.
|
|
3
|
+
> State Machines runs disposable environments of replica SaaS apps, such as Salesforce, for tests and coding agents. Start, connect to and stop them with the TypeScript SDK `@usestatemachines/sdk`.
|
|
4
4
|
|
|
5
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
6
|
|
|
7
7
|
## Concepts
|
|
8
8
|
|
|
9
|
-
- Environment: one disposable run of one or more apps.
|
|
10
|
-
-
|
|
11
|
-
- Actor:
|
|
12
|
-
-
|
|
9
|
+
- Environment: one disposable run of one or more apps. `const env = await sm.environments.start({ apps: ['salesforce'] })` creates it, waits until it is `running` and returns a `RunningEnvironment`. If the environment fails or the wait times out or is aborted, `start()` stops the environment and rethrows the original error; an `EnvironmentFailedError` or `TimeoutError` carries `err.environmentId` and `err.stopped`. Always call `env.stop()` in a `finally`.
|
|
10
|
+
- Apps: a string is shorthand for `{ key: s, appId: s }`, nothing more. Use the object form, such as `{ key: 'crm', appId: 'salesforce' }`, for your own key or two instances of one app. `appId` names the app in the catalog (`sm.catalog.get()`); `key` is your name for it in this environment, and later calls take the key.
|
|
11
|
+
- Actor: one of the app's users; requests run with that user's permissions. Salesforce actor IDs are its user IDs, such as `005…`, never a role name. `await env.actors('salesforce')` lists them in the release's order. connect() acts as the environment's declared default connection's actor, or else the first actor, the administrator, unless you pass `actorId`; pick a less-privileged actor to test permissions.
|
|
12
|
+
- API profile: one API of the app, with its path and versions. Salesforce offers several (`rest`, `soap-partner`, `bulk2`, `oauth`) and has no default, so always pass `apiId`: `env.connect('salesforce', { apiId: 'rest' })`.
|
|
13
|
+
- Connection: `connection.fetch` never sends your API key, never follows redirects and never retries. `connection.headers()` returns live tokens: treat them as secrets. Salesforce's SOAP profile (`soap-partner`) takes the actor token in the envelope instead: put `connection.actorToken()`, also a secret, in `SessionHeader/sessionId`; `headers()` then carries only `X-State-Environment-Token`.
|
|
14
|
+
- Full control: `sm.environments.create(input)` returns one `Environment` before the apps boot, `sm.environments.waitUntilReady(environment.id)` waits, and `sm.environments.connect(environment.id, key, options)` connects. Nothing on this path stops the environment for you; use it to keep a failed environment for inspection.
|
|
13
15
|
- 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
16
|
- Pagination: `environments.list`, `environments.events` and `workspaces.events` return one page, `{ data, nextCursor }`. Pass `nextCursor` back as `cursor` until it is `null`.
|
|
15
17
|
|
|
@@ -20,26 +22,26 @@ Enums are open: handle values you don't recognise.
|
|
|
20
22
|
- Environment `status`: `queued`, `starting`, `running`, `stopping`, `stopped`, `failed`. `stopReason`: `requested`, `expired`, `app_failed`, `owner_lost`.
|
|
21
23
|
- App `status`: `pending`, `starting`, `ready`, `stopping`, `stopped`, `failed`. A failed app carries `errorCode`.
|
|
22
24
|
- App `disposal`: `pending`, `complete`, `failed`, `unknown`.
|
|
23
|
-
- `waitUntilReady`
|
|
25
|
+
- `start()` and `waitUntilReady` throw `EnvironmentFailedError` if the environment becomes `failed`, `stopping` or `stopped`, and `TimeoutError` after 10 minutes by default (`readyTimeoutMs` for `start()`, `timeoutMs` for `waitUntilReady`). `start()` then stops the environment; `waitUntilReady` never does.
|
|
24
26
|
- `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
27
|
|
|
26
28
|
## Limits
|
|
27
29
|
|
|
28
30
|
- Live environments are capped per organization: `(await sm.credential.get()).limits.liveEnvironments`.
|
|
29
31
|
- `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`.
|
|
32
|
+
- Always call `env.stop()` in a `finally`. In CI, then await `env.waitUntilStopped()`.
|
|
31
33
|
|
|
32
34
|
## Errors and retries
|
|
33
35
|
|
|
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`
|
|
36
|
+
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`. `err.stopped === false` with `err.stopError` means the stop wasn't confirmed; `sm.environments.stop(err.environmentId)` is safe to repeat. Without `stopError`, you passed your own `idempotencyKey`, so `start()` left the environment running because another `start()` with that key may be using it; stop it only once nothing else needs it. Every error from `create`, including the create step of `start()`, carries `err.idempotencyKey`: `sm.environments.start(input, { idempotencyKey: err.idempotencyKey })` with the same input returns the environment the first call created. Invalid `apps` fail before any request as a `StateMachinesError` (`status` null, `cause` a `TypeError`; don't replay it) with one of: `Pass at least one app.`, `App key "<key>" is used twice.` or `"<s>" can't be used as an app key; pass { key, appId } instead.`
|
|
35
37
|
|
|
36
38
|
| Code | Status | What to do |
|
|
37
39
|
| --- | --- | --- |
|
|
38
|
-
| `environment-limit` | 429 | Not retried. Stop a live environment (`sm.environments.list({ status: 'live' })`), then
|
|
40
|
+
| `environment-limit` | 429 | Not retried. Stop a live environment (`sm.environments.list({ status: 'live' })`), then start again. |
|
|
39
41
|
| `lifetime-too-long` | 422 | Lower `lifetimeMinutes` to at most `limits.environmentLifetime.maximumMinutes`. |
|
|
40
42
|
| `release-unavailable` | 422 | Unknown or retired app or release. Check `appId` and `appReleaseId` against `sm.catalog.get()`. |
|
|
41
43
|
| `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,
|
|
44
|
+
| `connection-unavailable` | 409 | The app stopped or the environment is no longer `running`. If it stopped or expired, start a new one. (Before revealing any token, connect() throws a `TypeError` for an app that isn't ready, an unknown actor, or a missing `apiId`.) |
|
|
43
45
|
| `idempotency-conflict` | 409 | The idempotency key was used with a different input. Replay with the exact same input. |
|
|
44
46
|
| `request-timeout` | 503 | Retried automatically. If `create` still fails, replay it with `err.idempotencyKey`. |
|
|
45
47
|
|
|
@@ -50,38 +52,15 @@ Save it unchanged as `state-quickstart.mts` and run `npx tsx state-quickstart.mt
|
|
|
50
52
|
```ts
|
|
51
53
|
import { StateMachines } from '@usestatemachines/sdk';
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
const
|
|
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
|
-
});
|
|
55
|
+
const sm = new StateMachines(); // reads STATE_API_KEY
|
|
56
|
+
const env = await sm.environments.start({ apps: ['salesforce'] });
|
|
62
57
|
|
|
63
58
|
try {
|
|
64
|
-
await
|
|
65
|
-
|
|
66
|
-
|
|
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());
|
|
59
|
+
const salesforce = await env.connect('salesforce', { apiId: 'rest' });
|
|
60
|
+
const res = await salesforce.fetch('/services/data/v67.0/limits');
|
|
61
|
+
console.log(res.status, await res.json());
|
|
82
62
|
} finally {
|
|
83
|
-
|
|
84
|
-
await sm.environments.stop(environment.id);
|
|
63
|
+
await env.stop();
|
|
85
64
|
}
|
|
86
65
|
```
|
|
87
66
|
|
package/agents/prompt.md
CHANGED
|
@@ -1,49 +1,26 @@
|
|
|
1
1
|
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.
|
|
2
2
|
|
|
3
3
|
1. 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.
|
|
4
|
-
2. 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.
|
|
5
|
-
3. Save this quickstart unchanged as `state-quickstart.mts` and run `npx tsx state-quickstart.mts` to confirm the setup:
|
|
4
|
+
2. Check that `STATE_API_KEY` is set without printing it: `test -n "$STATE_API_KEY"`. If it is missing, stop and ask me to set it; the SDK doesn't read `.env` files. In Cloudflare Workers, pass the key as `new StateMachines({ apiKey })`.
|
|
5
|
+
3. Save this quickstart unchanged as `state-quickstart.mts` and run `npx tsx state-quickstart.mts` to confirm the setup. It prints `200` and the org's limits:
|
|
6
6
|
|
|
7
7
|
```ts
|
|
8
8
|
import { StateMachines } from '@usestatemachines/sdk';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
// A failed create carries err.idempotencyKey: see "Recovering a failed create" in the README.
|
|
14
|
-
const environment = await sm.environments.create({
|
|
15
|
-
name: 'quickstart',
|
|
16
|
-
// The default Salesforce app; sm.catalog.get() lists every app.
|
|
17
|
-
apps: [{ key: 'crm', appId: 'salesforce' }],
|
|
18
|
-
});
|
|
10
|
+
const sm = new StateMachines(); // reads STATE_API_KEY
|
|
11
|
+
const env = await sm.environments.start({ apps: ['salesforce'] });
|
|
19
12
|
|
|
20
13
|
try {
|
|
21
|
-
await
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// The connection lists the actors; this acts as the first.
|
|
25
|
-
const { actors } = await sm.environments.getConnection(environment.id, 'crm');
|
|
26
|
-
const [actor] = actors;
|
|
27
|
-
|
|
28
|
-
if (!actor) {
|
|
29
|
-
throw new Error('The Salesforce app has no actors.');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const crm = await sm.environments.connect(environment.id, 'crm', {
|
|
33
|
-
actorId: actor.id,
|
|
34
|
-
apiId: 'rest',
|
|
35
|
-
});
|
|
36
|
-
const response = await crm.fetch('/services/data/v67.0/limits');
|
|
37
|
-
|
|
38
|
-
console.log(response.status, await response.json());
|
|
14
|
+
const salesforce = await env.connect('salesforce', { apiId: 'rest' });
|
|
15
|
+
const res = await salesforce.fetch('/services/data/v67.0/limits');
|
|
16
|
+
console.log(res.status, await res.json());
|
|
39
17
|
} finally {
|
|
40
|
-
|
|
41
|
-
await sm.environments.stop(environment.id);
|
|
18
|
+
await env.stop();
|
|
42
19
|
}
|
|
43
20
|
```
|
|
44
21
|
|
|
45
|
-
4. Adapt it to my task. Log `await sm.catalog.get()` to find each app's
|
|
46
|
-
5. Always stop
|
|
47
|
-
6.
|
|
22
|
+
4. Adapt it to my task. Log `await sm.catalog.get()` to find each app's ID and the actors and API profiles its release offers; don't guess IDs. Start only the apps the task needs; a string such as `'salesforce'` means `{ key: 'salesforce', appId: 'salesforce' }`. Salesforce always needs an `apiId`. `connect()` acts as the declared default connection's actor, or else the app's first actor, the administrator; to act as a specific user, pass an `actorId` from `await env.actors('salesforce')`.
|
|
23
|
+
5. Always call `env.stop()` in a `finally`. In CI, then await `env.waitUntilStopped()` so the run fails if cleanup isn't confirmed. If `start()` fails after creating the environment, it stops it; an `EnvironmentFailedError` or `TimeoutError` carries `err.environmentId` and `err.stopped`. If the create itself fails, call `sm.environments.start(input, { idempotencyKey: err.idempotencyKey })` with the same `input`; never retry without the key, which can start a second environment.
|
|
24
|
+
6. Never print, log or commit keys or tokens, including `connection.headers()` and `connection.actorToken()`, which return live tokens.
|
|
48
25
|
|
|
49
26
|
Read more: `node_modules/@usestatemachines/sdk/README.md`, https://state-web.usestatemachines.workers.dev/llms.txt and https://www.npmjs.com/package/@usestatemachines/sdk
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CreateEnvironmentInput, CreateEnvironmentInputApp } from '../models/create-environment-input.js';
|
|
2
|
+
/** A string is shorthand for `{ key: <the string>, appId: <the string> }`. */
|
|
3
|
+
export type AppInput = string | CreateEnvironmentInputApp;
|
|
4
|
+
export interface EnvironmentInput extends Omit<CreateEnvironmentInput, 'apps'> {
|
|
5
|
+
/** 1 to 10 apps with unique keys. */
|
|
6
|
+
apps: AppInput[];
|
|
7
|
+
}
|
|
8
|
+
export declare function expandApps(input: EnvironmentInput): CreateEnvironmentInput;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const APP_KEY = /^[a-z][a-z0-9-]{0,39}$/;
|
|
2
|
+
export function expandApps(input) {
|
|
3
|
+
if (input.apps.length === 0) {
|
|
4
|
+
throw new TypeError('Pass at least one app.');
|
|
5
|
+
}
|
|
6
|
+
const apps = input.apps.map((app) => {
|
|
7
|
+
if (typeof app !== 'string') {
|
|
8
|
+
return app;
|
|
9
|
+
}
|
|
10
|
+
if (!APP_KEY.test(app)) {
|
|
11
|
+
throw new TypeError(`"${app}" can't be used as an app key; pass { key, appId } instead.`);
|
|
12
|
+
}
|
|
13
|
+
return { key: app, appId: app };
|
|
14
|
+
});
|
|
15
|
+
const keys = new Set();
|
|
16
|
+
for (const { key } of apps) {
|
|
17
|
+
if (keys.has(key)) {
|
|
18
|
+
throw new TypeError(`App key "${key}" is used twice.`);
|
|
19
|
+
}
|
|
20
|
+
keys.add(key);
|
|
21
|
+
}
|
|
22
|
+
return { ...input, apps };
|
|
23
|
+
}
|
|
@@ -2,15 +2,17 @@ import { type Fetcher, HTTPClient } from '../lib/http.js';
|
|
|
2
2
|
import type { Catalog } from '../models/catalog.js';
|
|
3
3
|
import type { Connection as ConnectionDetails } from '../models/connection.js';
|
|
4
4
|
import type { ConnectionSecret } from '../models/connection-secret.js';
|
|
5
|
-
import type { CreateEnvironmentInput } from '../models/create-environment-input.js';
|
|
6
5
|
import type { Credential } from '../models/credential.js';
|
|
7
6
|
import type { Environment } from '../models/environment.js';
|
|
8
7
|
import type { EventPage } from '../models/event-page.js';
|
|
9
8
|
import type { ListEnvironmentsRequest } from '../models/operations/list-environments.js';
|
|
10
9
|
import type { ListWorkspacesRequest } from '../models/operations/list-workspaces.js';
|
|
11
10
|
import type { Release } from '../models/release.js';
|
|
11
|
+
import type { ReleaseActor } from '../models/release-actor.js';
|
|
12
12
|
import type { Workspace } from '../models/workspace.js';
|
|
13
|
+
import { type EnvironmentInput } from './apps.js';
|
|
13
14
|
import { Connection } from './connection.js';
|
|
15
|
+
import { RunningEnvironment } from './running-environment.js';
|
|
14
16
|
import { type WaitOptions } from './wait.js';
|
|
15
17
|
export interface StateMachinesOptions {
|
|
16
18
|
/** Workspace API key. Defaults to `STATE_API_KEY` where a `process` exists. */
|
|
@@ -38,8 +40,14 @@ export interface Page<T> {
|
|
|
38
40
|
data: T[];
|
|
39
41
|
nextCursor: string | null;
|
|
40
42
|
}
|
|
43
|
+
export interface StartOptions extends CreateOptions {
|
|
44
|
+
/** Deadline for the whole wait for readiness. Defaults to 10 minutes. */
|
|
45
|
+
readyTimeoutMs?: number;
|
|
46
|
+
}
|
|
41
47
|
export interface ConnectOptions extends CallOptions {
|
|
48
|
+
/** Defaults to the actor of the environment's declared default connection, else the app's first actor. */
|
|
42
49
|
actorId?: string;
|
|
50
|
+
/** Required when the app has more than one API, as Salesforce does. */
|
|
43
51
|
apiId?: string;
|
|
44
52
|
}
|
|
45
53
|
export declare class StateMachines {
|
|
@@ -63,19 +71,41 @@ export declare class StateMachines {
|
|
|
63
71
|
readonly environments: {
|
|
64
72
|
list: (query?: ListEnvironmentsQuery, options?: CallOptions | undefined) => Promise<Page<Environment>>;
|
|
65
73
|
get: (environmentId: string, options?: CallOptions | undefined) => Promise<Environment>;
|
|
66
|
-
/**
|
|
67
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Creates an environment and waits until it's running. If the wait fails, stops the
|
|
76
|
+
* environment (unless the caller passed its own `idempotencyKey`), then rethrows the
|
|
77
|
+
* error with `environmentId`, `stopped` and `stopError`.
|
|
78
|
+
*/
|
|
79
|
+
start: (input: EnvironmentInput, options?: StartOptions) => Promise<RunningEnvironment>;
|
|
80
|
+
/**
|
|
81
|
+
* A string app is shorthand for `{ key: <the string>, appId: <the string> }`. Every
|
|
82
|
+
* failure carries the `idempotencyKey`; replaying the create with it is safe.
|
|
83
|
+
*/
|
|
84
|
+
create: (input: EnvironmentInput, options?: CreateOptions) => Promise<Environment>;
|
|
68
85
|
stop: (environmentId: string, options?: CallOptions | undefined) => Promise<Environment>;
|
|
69
86
|
events: (environmentId: string, query?: {
|
|
70
87
|
limit?: number;
|
|
71
88
|
cursor?: string;
|
|
72
89
|
}, options?: CallOptions | undefined) => Promise<EventPage>;
|
|
90
|
+
/** The app's users, in the release's order. */
|
|
91
|
+
actors: (environmentId: string, appKey: string, options?: CallOptions | undefined) => Promise<ReleaseActor[]>;
|
|
73
92
|
getConnection: (environmentId: string, appKey: string, options?: CallOptions | undefined) => Promise<ConnectionDetails>;
|
|
74
93
|
reveal: (environmentId: string, appKey: string, actorId: string, options?: CallOptions | undefined) => Promise<ConnectionSecret>;
|
|
75
94
|
/** Resolves when the environment is running. Never stops it. */
|
|
76
95
|
waitUntilReady: (environmentId: string, options?: WaitOptions | undefined) => Promise<Environment>;
|
|
77
96
|
/** Resolves when the environment ended and every sandbox is confirmed disposed. */
|
|
78
97
|
waitUntilStopped: (environmentId: string, options?: WaitOptions | undefined) => Promise<Environment>;
|
|
98
|
+
/**
|
|
99
|
+
* actorId defaults to the actor of the environment's declared default connection, or
|
|
100
|
+
* else the app's first actor (for Salesforce, the administrator). Pick another from
|
|
101
|
+
* actors() to act with that user's permissions.
|
|
102
|
+
*/
|
|
79
103
|
connect: (environmentId: string, appKey: string, options?: ConnectOptions) => Promise<Connection>;
|
|
80
104
|
};
|
|
81
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Names the call a request belongs to. Never sent: the attempt hook removes it.
|
|
108
|
+
* Needed because the generated loop clones each attempt, and after a garbage
|
|
109
|
+
* collection a clone's signal no longer follows the call's (Node 20 and 24).
|
|
110
|
+
*/
|
|
111
|
+
export declare const CALL_HEADER = "x-statemachines-call";
|