diff --git a/.github/workflows/publish-python-sdk.yml b/.github/workflows/publish-python-sdk.yml
deleted file mode 100644
index a1c6e0f4b46..00000000000
--- a/.github/workflows/publish-python-sdk.yml
+++ /dev/null
@@ -1,108 +0,0 @@
-name: publish-python-sdk
-
-on:
- push:
- branches: [main]
- paths:
- - 'packages/python-sdk/**'
-
-permissions:
- contents: write
-
-# A newer push waits for the running publish rather than racing its version check.
-concurrency:
- group: publish-python-sdk-${{ github.ref }}
- cancel-in-progress: false
-
-jobs:
- publish:
- name: publish
- runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
- timeout-minutes: 15
- steps:
- - name: Checkout repository
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
- with:
- persist-credentials: false
-
- - name: Setup Python
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
- with:
- python-version: '3.12'
- cache: 'pip'
-
- - name: Install build dependencies
- run: |
- python -m pip install --upgrade pip
- pip install build twine pytest requests tomli
-
- - name: Run tests
- working-directory: packages/python-sdk
- run: |
- PYTHONPATH=. pytest tests/ -v
-
- - name: Get package version
- id: package_version
- working-directory: packages/python-sdk
- run: echo "version=$(python -c "import tomli; print(tomli.load(open('pyproject.toml', 'rb'))['project']['version'])")" >> $GITHUB_OUTPUT
-
- - name: Check if version already exists
- id: version_check
- # Exact lookup: PyPI answers 404 only for a version it has never published. Anything other
- # than 200 or 404 stops the job rather than guessing.
- env:
- VERSION: ${{ steps.package_version.outputs.version }}
- run: |
- status="$(curl -sS -o /dev/null -w '%{http_code}' "https://pypi.org/pypi/simstudio-sdk/$VERSION/json")"
- case "$status" in
- 200) echo "exists=true" >> "$GITHUB_OUTPUT" ;;
- 404) echo "exists=false" >> "$GITHUB_OUTPUT" ;;
- *) echo "::error::PyPI returned HTTP $status for simstudio-sdk $VERSION" >&2; exit 1 ;;
- esac
-
- - name: Build package
- if: steps.version_check.outputs.exists == 'false'
- working-directory: packages/python-sdk
- run: python -m build
-
- - name: Check package
- if: steps.version_check.outputs.exists == 'false'
- working-directory: packages/python-sdk
- run: twine check dist/*
-
- - name: Publish to PyPI
- if: steps.version_check.outputs.exists == 'false'
- working-directory: packages/python-sdk
- env:
- TWINE_USERNAME: __token__
- TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
- run: twine upload dist/*
-
- - name: Log skipped publish
- if: steps.version_check.outputs.exists == 'true'
- env:
- VERSION: ${{ steps.package_version.outputs.version }}
- run: echo "Skipped publishing because version $VERSION already exists on PyPI"
-
- - name: Create GitHub Release
- if: steps.version_check.outputs.exists == 'false'
- uses: softprops/action-gh-release@8a0cd651e5b2365bd640854a3fa5dfc70e4875f3 # v3.0.4
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- tag_name: python-sdk-v${{ steps.package_version.outputs.version }}
- name: Python SDK v${{ steps.package_version.outputs.version }}
- body: |
- ## Python SDK v${{ steps.package_version.outputs.version }}
-
- Published simstudio-sdk==${{ steps.package_version.outputs.version }} to PyPI.
-
- ### Installation
- ```bash
- pip install simstudio-sdk==${{ steps.package_version.outputs.version }}
- ```
-
- ### Documentation
- See the [README](https://github.com/simstudioai/sim/tree/main/packages/python-sdk) or the [docs](https://docs.sim.ai/sdks/python) for more information.
- draft: false
- prerelease: false
\ No newline at end of file
diff --git a/.github/workflows/publish-ts-sdk.yml b/.github/workflows/publish-ts-sdk.yml
deleted file mode 100644
index 15f3bbb51b4..00000000000
--- a/.github/workflows/publish-ts-sdk.yml
+++ /dev/null
@@ -1,141 +0,0 @@
-name: publish-ts-sdk
-
-on:
- push:
- branches: [main]
- paths:
- - 'packages/ts-sdk/**'
-
-permissions:
- contents: read
-
-# A newer push waits for the running publish rather than racing its version check.
-concurrency:
- group: publish-ts-sdk-${{ github.ref }}
- cancel-in-progress: false
-
-jobs:
- build:
- name: build
- runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
- timeout-minutes: 15
- outputs:
- version: ${{ steps.package_version.outputs.version }}
- exists: ${{ steps.version_check.outputs.exists }}
- steps:
- - name: Checkout repository
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
- with:
- persist-credentials: false
-
- - name: Setup workspace
- uses: ./.github/actions/setup
- with:
- provider: ${{ vars.CI_PROVIDER }}
- node-version: '22'
-
- - name: Run tests
- working-directory: packages/ts-sdk
- run: bun run test
-
- - name: Build package
- working-directory: packages/ts-sdk
- run: bun run build
-
- - name: Get package version
- id: package_version
- working-directory: packages/ts-sdk
- run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
-
- - name: Check if version already exists
- id: version_check
- env:
- VERSION: ${{ steps.package_version.outputs.version }}
- run: |
- if npm view "simstudio-ts-sdk@$VERSION" version &> /dev/null; then
- echo "exists=true" >> "$GITHUB_OUTPUT"
- else
- echo "exists=false" >> "$GITHUB_OUTPUT"
- fi
-
- # Bun packs because it rewrites `workspace:` ranges to real versions,
- # which `npm pack` would publish verbatim.
- - name: Pack package
- if: steps.version_check.outputs.exists == 'false'
- working-directory: packages/ts-sdk
- run: bun pm pack --ignore-scripts --filename "$RUNNER_TEMP/package.tgz" --quiet
-
- - name: Upload package
- if: steps.version_check.outputs.exists == 'false'
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
- with:
- name: package
- path: ${{ runner.temp }}/package.tgz
- if-no-files-found: error
- retention-days: 1
-
- - name: Log skipped publish
- if: steps.version_check.outputs.exists == 'true'
- env:
- VERSION: ${{ steps.package_version.outputs.version }}
- run: echo "Skipped publishing because version $VERSION already exists on npm"
-
- # npm trusted publishing trades this job's OIDC token for a short-lived npm
- # credential, so no npm token is stored anywhere. npm accepts the token only
- # from GitHub-hosted runners, and only this job can mint one: it runs no
- # repository code, it publishes the tarball the build job tested. npm runs no
- # lifecycle scripts for a tarball, so nothing is rebuilt here, and provenance
- # is attached automatically.
- publish:
- name: publish
- needs: build
- if: needs.build.outputs.exists == 'false'
- runs-on: ubuntu-latest
- timeout-minutes: 10
- permissions:
- id-token: write
- steps:
- # Node 24 bundles npm 11.5.1+, which trusted publishing requires.
- - name: Setup Node
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
- with:
- node-version: '24'
-
- - name: Download package
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
- with:
- name: package
- path: ${{ runner.temp }}
-
- - name: Publish to npm
- run: npm publish "$RUNNER_TEMP/package.tgz" --access public
-
- release:
- name: release
- needs: [build, publish]
- runs-on: ubuntu-latest
- timeout-minutes: 5
- permissions:
- contents: write
- steps:
- - name: Create GitHub Release
- uses: softprops/action-gh-release@8a0cd651e5b2365bd640854a3fa5dfc70e4875f3 # v3.0.4
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- tag_name: typescript-sdk-v${{ needs.build.outputs.version }}
- name: TypeScript SDK v${{ needs.build.outputs.version }}
- body: |
- ## TypeScript SDK v${{ needs.build.outputs.version }}
-
- Published simstudio-ts-sdk@${{ needs.build.outputs.version }} to npm.
-
- ### Installation
- ```bash
- npm install simstudio-ts-sdk@${{ needs.build.outputs.version }}
- ```
-
- ### Documentation
- See the [README](https://github.com/simstudioai/sim/tree/main/packages/ts-sdk) or the [docs](https://docs.sim.ai/sdks/typescript) for more information.
- draft: false
- prerelease: false
diff --git a/apps/docs/COPY_AUDIT.md b/apps/docs/COPY_AUDIT.md
index 2691adeb91a..1cf3934e02d 100644
--- a/apps/docs/COPY_AUDIT.md
+++ b/apps/docs/COPY_AUDIT.md
@@ -637,8 +637,6 @@ Every row below represents a fully reviewed inventory entry. “Rewritten” inc
| --- | --- | --- | --- |
| [api-reference/authentication.mdx](content/docs/api-reference/authentication.mdx) | rewritten | Separated credential identity/current access from workspace-key management and operation policies; retained workspace payer/member attribution and personal/OAuth restrictions. Retained verified one-time key display/create/revoke instructions and exact Sim API keys label; qualified encryption and modern/legacy prefixes by API_ENCRYPTION_KEY. Kept API-key versus OAuth headers, scope table, method-independent scopes, account grant versus login revocation and API-key logout caveat. Replaced duplicate three-language request with curl and canonical Getting started/SDK links; folded repeated credential-safety text. | Verified improvement. Distinguishes acting identity, current role/policy, operation-specific workspace-key access, and payer instead of the baseline vague permissions table. Corrects API-key Bearer misuse, optional encryption/legacy prefix, OAuth scope/grant and logout behavior; removes duplicate HTTP language samples while linking complete versions. Preserved: Retains key creation/revocation/one-time display, X-API-Key, server-side credentials, modern prefix and OAuth scopes. Complete HTTP examples still exist in Getting started; OAuth/profile detail remains in CLI authentication. |
| [api-reference/getting-started.mdx](content/docs/api-reference/getting-started.mdx) | rewritten | Separated Cloud/self-host origins, active deployment prerequisites, manual draft access, HTTP result versus in-band run failures, and request versus execution rate policies. Added checked env-backed direct requests, all current wait/terminal/pause states, async mode compatibility and client/server timeout distinction. Preserved one-shot X-Run-Id409 behavior and uncertain-run reconciliation; fixed pagination with required workspaceId and URL-encoded returned cursor. Preserved resource/list/error shapes, strict undeclared-field behavior and validation details while distinguishing OAuth401, usage402, conflict409 and USD/credits. Kept substantial first-call Steps/language Tabs; removed routine status/billing callouts. API navigation moves advanced workflow-sync after SDKs under API guides. Used shared authored tabs with padded mixed content, wrapped prose, visible horizontal scrolling, and mobile touch targets. | Verified improvement. Examples now define environment prerequisites and handle HTTP and in-band run failures; manual draft execution is distinguished from the active deployment. Explains actual status/pause/redaction, async option compatibility, one-shot run-ID recovery, endpoint-specific units, and bound cursor queries. Preserved: Retains origin/OpenAPI URL, API-key creation, ID lookup, deployment, three HTTP languages, async run/status URL, envelopes, strict unknown-field validation, pagination and Retry-After. |
-| [api-reference/python.mdx](content/docs/api-reference/python.mdx) | rewritten | Retained every public method signature/default and complete return-field reference, added forced-sync method and missing retry timeout parameter. Replaced repeated Quick Start/Basic/Steps/env tabs with env-backed context manager; retained sequential multiple-workflow, file-like upload, async polling, retry/status/usage examples. Explained Python failed result versus exception, completed/paused/cancelled states, metadata units and unset logs/traces; preserved 0.2.x migration and mixed legacy/v2 helpers. Fixed usage requestsPerMinute/maxBurst fields and account/key-owner USD scope; Python storage omission explicit; corrected v2 reset milliseconds with legacy-unit caveat. Replaced broken SDK streaming example and done-event parser with raw HTTP SSE inspection and canonical framing/forwarding instructions; preserved Flask backend authentication, event preservation, status/timeout/cleanup concerns. Added redacting to polling and local wait deadline, distinguished client/server timeouts, output selectors, run fields/files/permissions and opaque large-value references. | Verified improvement. Replaces the misleading async/streaming/readiness claims with blocking HTTP, SDK-vs-HTTP streaming limits and failed/paused/cancelled result behavior. Keeps full method and return reference while consolidating repeated examples; corrects retry/reset units and usage shape. Preserved: Retains Python/requests requirements, context management/close, method options/defaults, types, baseURL, file inputs, async polling, retry, usage helpers and Apache-2.0 link. Legacy status/jobs and SDK migration remain explicit; runnable examples replace duplicate/incorrect samples. |
-| [api-reference/typescript.mdx](content/docs/api-reference/typescript.mdx) | rewritten | Retained all public method signatures/options and complete declared/runtime return fields, added forced-sync convenience and LargeValueRef/legacyJob status distinctions. Used Bun install and Node18+ ESM requirement, scoped automatic globalFile conversion to Node20+, fixed Buffer-to-BlobPart typing using Uint8Array. Explained failed-run rejection versus cancelled/paused resolution and exact-state run lookup; async polling waits through runtime redacting without widening SDK types. Fixed legacy USD usage fields/storage, v2 reset-millisecond example, client wait versus server cap, rate-only retries and unsafe-retry recovery. Replaced broken SDK streaming and stale done-frame example with raw HTTP stream, terminal/reset handling, chunk boundaries and canonical protocol client links. Folded duplicate initialization/Steps/environment tabs into one setup, preserved backend helper and Express/Next.js POST/auth/input-size/status responsibilities; retained file formats and actual upload example. | Verified improvement. Corrects Node minimum/global File availability, failed-run rejection, paused success, absent synchronous status, unsupported SDK streaming, and timeout-without-abort. Preserves complete declarations and options while reducing repeated implementations; corrects retries/usage/reset units and shows HTTP framing. Preserved: Retains constructor and methods, options/defaults, types, setter/helpers, file and URL objects, server/backend authorization, async polling, retry, usage, legacy compatibility and license link. |
| [api-reference/workflow-sync.mdx](content/docs/api-reference/workflow-sync.mdx) | rewritten | Reorganized prerequisites/import/fork/sync/verification/recovery and moved advanced navigation; retained every command/endpoint relationship and scope/role distinction. Made billing-enabled Enterprise versus billing-disabled deployment gate explicit; qualified export sanitization and kept unresolved env values/provenance non-authority. Retained source kind mappings, flat occurrence bindings/encodings/positions, null/required binding rules, dependentValues and selector scope/context/partial/restart bounds. Preserved fork drafts/explicit copies, push/pull direction/direct edge, mapping read/write projection, omitted/replaced/cleared saved choices and trigger URL adoption. Preserved request-ID/fingerprint apply commands, atomic import/idMap/plain-import exception, receipt workspace ownership, committed-follow-up failure and exact deployment readiness. Kept uncertainty reconciliation/stale preview recovery, rollback draft/copy limitations, unlink/exclusions; moved contributor harness details to existing canonical repository README link. | Verified improvement. Puts permissions and deployment gates before mutation steps; separates export/import, fork, direction-relative sync, receipt/recovery, and rollback consequences. Corrects confidentiality guarantees and adds selector bounds, fingerprint/retry safeguards, copy identity differences and exact deployment readiness. Preserved: Retains CLI↔HTTP map, all import/sync payload shapes, source IDs, nested occurrences/encodings, destination selectors, trigger adoption, request IDs, receipts and retry recovery. Contributor harness detail moved to a linked canonical README that contains Bun/Docker/PostgreSQL17/fixtures/migration boundary. |
### Knowledgebase (7)
diff --git a/apps/docs/app/llms.txt/route.ts b/apps/docs/app/llms.txt/route.ts
index 6cbf1c069a0..b5b552cfde8 100644
--- a/apps/docs/app/llms.txt/route.ts
+++ b/apps/docs/app/llms.txt/route.ts
@@ -41,7 +41,7 @@ Sim is the open-source AI workspace where teams build, deploy, and manage AI age
- Run a deployed workflow: \`POST ${SIM_SITE_URL}/api/v2/workflows/{workflowId}/execute\` with a JSON body like \`{ "input": { "fieldName": "value" } }\`.
- Authenticate API keys with the \`X-API-Key\` header. \`Authorization: Bearer\` accepts only Sim OAuth access tokens, never API keys.
- Responses wrap the payload in \`data\`. A synchronous run returns \`200 { "data": { "runId", "status", "output", ... } }\`; with \`"async": true\` in the body it returns \`202 { "data": { "runId", "statusUrl" } }\` and you poll \`statusUrl\`.
-- SDKs: \`simstudio-ts-sdk\` (npm) and \`simstudio-sdk\` (PyPI). Full reference: [API getting started](${baseUrl}/api-reference/getting-started).
+- Full reference: [API getting started](${baseUrl}/api-reference/getting-started).
## Documentation Overview
diff --git a/apps/docs/content/docs/api-reference/authentication.mdx b/apps/docs/content/docs/api-reference/authentication.mdx
index ea3279bf00a..6b3da616caf 100644
--- a/apps/docs/content/docs/api-reference/authentication.mdx
+++ b/apps/docs/content/docs/api-reference/authentication.mdx
@@ -5,7 +5,7 @@ description: Choose an API key or delegated OAuth token and understand its acces
import { Callout } from '@/components/ui/callout'
-Authenticate API requests with a Sim API key, or an OAuth access token when your deployment enables OAuth. The Python and TypeScript SDKs use API keys. The CLI and registered OAuth applications can act with the permissions you approve.
+Authenticate API requests with a Sim API key, or an OAuth access token when your deployment enables OAuth. The CLI and registered OAuth applications can act with the permissions you approve.
## Choose a key [#key-types]
@@ -45,7 +45,7 @@ curl --fail-with-body -X POST \
-d '{"input": {}}'
```
-This runs the active deployment. Use your deployment's origin for self-hosted requests. [Getting started](/api-reference/getting-started) includes complete HTTP examples and response handling; [Python](/api-reference/python) and [TypeScript](/api-reference/typescript) show SDK calls.
+This runs the active deployment. Use your deployment's origin for self-hosted requests. [Getting started](/api-reference/getting-started) includes complete HTTP examples and response handling.
Keys authenticate workflow execution, logs, deployed MCP servers, and other API resources that accept their key type. See each endpoint for its required access.
@@ -72,8 +72,6 @@ Scopes limit the application's actions; your current workspace membership, role,
Manage grants in **Settings → General → Authorized apps**. Revoking an application revokes all its logins for your account. For an OAuth login, `sim logout` revokes the current login and removes it locally; the CLI reports if server revocation fails. Removing a locally stored API key does not revoke that key on the server.
-The Python and TypeScript SDKs do not manage OAuth sign-in or token refresh.
-
## Protect credentials [#security]
Keep API keys and OAuth tokens on your server. A browser application should call an authenticated backend that checks which resources the caller may use before sending a Sim request. Do not put keys in client-side code or source control.
@@ -85,4 +83,3 @@ Sim supports modern `sk-sim-` and legacy `sim_` API keys. On self-hosted deploym
- [API getting started](/api-reference/getting-started)
- [CLI authentication](/cli/authentication)
- [Permissions](/platform/permissions)
-- [Python SDK](/api-reference/python) and [TypeScript SDK](/api-reference/typescript)
diff --git a/apps/docs/content/docs/api-reference/getting-started.mdx b/apps/docs/content/docs/api-reference/getting-started.mdx
index ad8a2acf426..9d0b352682e 100644
--- a/apps/docs/content/docs/api-reference/getting-started.mdx
+++ b/apps/docs/content/docs/api-reference/getting-started.mdx
@@ -14,7 +14,7 @@ Sim Cloud uses `https://www.sim.ai`, with API paths under `/api/v2`. There is no
Download the [OpenAPI 3.1 specification](/openapi.json) for client generation, request validation, and API tooling.
-Official SDKs wrap these calls: [TypeScript](/api-reference/typescript) (`bun add simstudio-ts-sdk`) and [Python](/api-reference/python) (`pip install simstudio-sdk`). The [CLI](/cli) (`bun add -g sim`) runs workflows and manages resources from a terminal.
+The [CLI](/cli) (`bun add -g sim`) runs workflows and manages resources from a terminal.
## Run a deployed workflow [#quick-start]
@@ -243,6 +243,5 @@ A rate-limited request returns `429`; `Retry-After` gives seconds to wait. Inspe
## Related docs
- [Authentication](/api-reference/authentication)
-- [Python SDK](/api-reference/python) and [TypeScript SDK](/api-reference/typescript)
- [Execute workflow](/api-reference/workflows/executeWorkflowV2)
- [Workflow imports and workspace sync](/api-reference/workflow-sync)
diff --git a/apps/docs/content/docs/api-reference/meta.json b/apps/docs/content/docs/api-reference/meta.json
index 3f5de5114b6..413272d21ab 100644
--- a/apps/docs/content/docs/api-reference/meta.json
+++ b/apps/docs/content/docs/api-reference/meta.json
@@ -5,9 +5,6 @@
"---Getting started---",
"getting-started",
"authentication",
- "---SDKs---",
- "python",
- "typescript",
"---API guides---",
"workflow-sync",
"---Endpoints---",
diff --git a/apps/docs/content/docs/api-reference/python.mdx b/apps/docs/content/docs/api-reference/python.mdx
deleted file mode 100644
index e137b1e4689..00000000000
--- a/apps/docs/content/docs/api-reference/python.mdx
+++ /dev/null
@@ -1,402 +0,0 @@
----
-title: Python
-description: Execute deployed workflows from Python, poll runs, and handle SDK results.
----
-
-import { Callout } from '@/components/ui/callout'
-
-Use `SimStudioClient` to execute deployed workflows from a Python application. It supports synchronous requests, server-queued asynchronous runs, rate-limit retries, and file-like inputs. Its HTTP methods are blocking; asynchronous execution does not make it an `asyncio` client.
-
-## Install and configure [#installation]
-
-The package requires Python `3.8+` and `requests>=2.25.0`:
-
-```bash
-pip install simstudio-sdk
-```
-
-Create a key in **Account settings → Sim API keys**, or use a workspace key if your administrator requires one. Keep it on your server as `SIM_API_KEY`. Set `WORKFLOW_ID` to a deployed workflow you can access. See [Authentication](/api-reference/authentication) for key types and permissions.
-
-### Server compatibility
-
-SDK `0.2.x` executes and polls through `/api/v2`. Your deployment must serve `POST /api/v2/workflows/{workflowId}/execute`; the SDK does not fall back when that route is unavailable. Upgrade an older self-hosted server, or intentionally retain `simstudio-sdk<0.2` for its legacy execution endpoints.
-
-The default origin is `https://www.sim.ai`. Set `base_url` to your self-hosted deployment's origin. Versions before `0.2.1` defaulted to `https://sim.ai`; set the `www` origin explicitly when using an older SDK against Sim Cloud to avoid redirects.
-
-## Run a workflow [#quick-start]
-
-Use a context manager to close the HTTP session automatically:
-
-```python
-import os
-from simstudio import SimStudioClient, SimStudioError
-
-with SimStudioClient(
- api_key=os.environ["SIM_API_KEY"],
- base_url=os.getenv("SIM_BASE_URL", "https://www.sim.ai"),
-) as client:
- try:
- result = client.execute_workflow_sync(
- os.environ["WORKFLOW_ID"],
- input={"message": "Process this data"},
- timeout=30.0,
- )
- if result.status == "completed":
- print(result.output)
- print("Duration in ms:", result.total_duration)
- elif result.status == "paused":
- print("The workflow is waiting to resume.")
- else:
- print(result.status, result.error)
- except SimStudioError as error:
- print(error.code, error.status, str(error))
- raise
-```
-
-Replace `message` with an input field defined by your deployed trigger. The SDK runs the active deployment; it does not select manual draft execution.
-
-A failed synchronous workflow returns normally with `status="failed"`, `success=False`, and `error` set. A cancelled run also returns `success=False`, often without an error. `success=True` means `completed` **or** `paused`; a pause is unfinished work. HTTP, authentication, and client timeout errors raise `SimStudioError`. This differs from the TypeScript SDK, which throws for a failed synchronous run.
-
-## Start an asynchronous run [#async-workflow-execution]
-
-Queue a deployed run and poll its `run_id`. The initial `success=True` confirms acceptance, not completion:
-
-```python
-import os
-import time
-from simstudio import AsyncExecutionResult, SimStudioClient
-
-waiting = {"queued", "pending", "running", "redacting"}
-workflow_id = os.environ["WORKFLOW_ID"]
-
-with SimStudioClient(api_key=os.environ["SIM_API_KEY"]) as client:
- result = client.execute_workflow(
- workflow_id,
- input={"message": "Process this dataset"},
- async_execution=True,
- execution_timeout_seconds=3600,
- )
- if not isinstance(result, AsyncExecutionResult):
- raise RuntimeError("Expected an asynchronous run receipt")
-
- deadline = time.monotonic() + 300
- while True:
- run = client.get_workflow_run(workflow_id, result.run_id, include_output=True)
- if run["status"] not in waiting:
- break
- if time.monotonic() >= deadline:
- raise TimeoutError(f"Stop waiting locally; run {result.run_id} may still be active")
- time.sleep(2)
-
- if run["status"] == "completed":
- print(run["output"])
- elif run["status"] == "paused":
- print("Resume condition:", run["paused"])
- else:
- print(run["status"], run["error"])
-```
-
-The five-minute polling deadline is an application choice; it does not cancel the run. A time pause can resume automatically, while a human pause requires input. Resume can create another run attempt. Use [Get workflow run](/api-reference/workflow-runs/getWorkflowRunV2) and the [resume endpoint](/api-reference/workflow-runs/resumeWorkflowRunV2) for the current state and resume behavior.
-
-Async submission cannot include `stream=True`, `selected_outputs`, or other output-shaping options. `execution_timeout_seconds` accepts an integer from `1` to `604800`, requires async execution, and only shortens the account policy. The HTTP `timeout` remains separate.
-
-## Handle errors and rate limits [#error-handling]
-
-`SimStudioError(message, code=None, status=None)` exposes `code` and the HTTP `status` when available. A workflow failure returned in `WorkflowExecutionResult` must be handled separately from exceptions.
-
-| Code | Meaning |
-| --- | --- |
-| `UNAUTHORIZED` | Missing, invalid, or expired credential |
-| `BAD_REQUEST` | V2 request validation failed |
-| `TIMEOUT` | The client stopped waiting |
-| `RATE_LIMIT_EXCEEDED` | SDK wrapper for an HTTP `429` |
-| `USAGE_LIMIT_EXCEEDED` | Usage policy rejected the request or run |
-| `INVALID_EXECUTION_TIMEOUT` | An unsupported or invalid async timeout option |
-| `EXECUTION_ERROR` | Transport, parsing, or execution-request failure |
-| `STATUS_ERROR`, `USAGE_ERROR` | A status or usage helper failed |
-
-Other server codes can pass through. V2 uses `BAD_REQUEST` for invalid JSON/request input, rather than the legacy `INVALID_JSON` branch. See [API errors](/api-reference/getting-started#error-handling).
-
-`execute_with_retry` retries only rate-limit exceptions. Its defaults allow three retries after the initial attempt, with exponential delay and ±25% jitter. A supplied `Retry-After` takes precedence over the calculated delay; jitter applies to that wait too.
-
-```python
-import os
-from simstudio import SimStudioClient
-
-with SimStudioClient(api_key=os.environ["SIM_API_KEY"]) as client:
- result = client.execute_with_retry(
- os.environ["WORKFLOW_ID"],
- input={"message": "Process this"},
- timeout=30.0,
- max_retries=3,
- initial_delay=1.0,
- max_delay=30.0,
- backoff_multiplier=2.0,
- )
- print(result)
-```
-
-`initial_delay` and `max_delay` are **seconds**. `RateLimitInfo.retry_after` is **milliseconds**. `max_delay` caps the calculated backoff, not a server-supplied wait.
-
-
-A client timeout or lost response does not prove the workflow never started. Avoid blindly repeating an execution that may have effects. For caller-chosen run IDs and reconciliation, use the raw v2 API described in [Getting started](/api-reference/getting-started#sync-vs-async-execution).
-
-
-## Method reference [#api-reference]
-
-### Constructor
-
-```text
-SimStudioClient(api_key: str, base_url: str = "https://www.sim.ai")
-```
-
-`api_key` is required. `base_url` is an origin, without an `/api` suffix; trailing slashes are removed. The client sends the key in `X-API-Key`.
-
-### Execute
-
-```text
-execute_workflow(workflow_id: str, input=None, *, timeout=30.0,
- stream=None, selected_outputs=None, async_execution=None,
- execution_timeout_seconds=None) -> WorkflowExecutionResult | AsyncExecutionResult
-
-execute_workflow_sync(workflow_id: str, input=None, *, timeout=30.0,
- stream=None, selected_outputs=None) -> WorkflowExecutionResult
-
-execute_with_retry(workflow_id: str, input=None, *, timeout=30.0,
- stream=None, selected_outputs=None, async_execution=None,
- execution_timeout_seconds=None, max_retries=3, initial_delay=1.0,
- max_delay=30.0, backoff_multiplier=2.0)
- -> WorkflowExecutionResult | AsyncExecutionResult
-```
-
-All options after `*` are keyword-only. `execute_workflow_sync` forces `async_execution=False`; the retry method accepts the same execution options as `execute_workflow`.
-
-| Parameter | Behavior and default |
-| --- | --- |
-| `workflow_id` | Deployed workflow ID |
-| `input` | Optional value; a dictionary becomes the v2 `input` object, while a primitive or list becomes `{"input": value}` inside it; omitted/`None` becomes `{}` |
-| `timeout` | Client HTTP timeout in seconds; default `30.0` |
-| `async_execution` | Default omitted, so server execution is synchronous; `True` returns an async receipt |
-| `execution_timeout_seconds` | Async-only integer cap, `1`–`604800`; cannot extend account policy |
-| `selected_outputs` | Optional list of normalized block-name/path selectors such as `agent.content`; incompatible with async submission |
-| `stream` | Defaults omitted/false; the current SDK parser cannot consume the SSE response when `True`; use raw HTTP below |
-| `max_retries` | Retry count after the initial attempt; default `3` |
-| `initial_delay`, `max_delay` | Calculated retry delay in seconds; defaults `1.0`, `30.0` |
-| `backoff_multiplier` | Delay multiplier; default `2.0` |
-
-File-like objects nested in input are converted to base64. The current synchronous adapter does not expose v2 `blockOutputs`, even when `selected_outputs` is sent. Use a run read or raw HTTP when you need that map.
-
-### Read workflow deployment and runs
-
-```text
-get_workflow_status(workflow_id: str) -> WorkflowStatus
-validate_workflow(workflow_id: str) -> bool
-get_workflow_run(workflow_id: str, run_id: str, *,
- include_output=None, selected_outputs=None) -> Dict[str, Any]
-get_job_status(job_id: str) -> Dict[str, Any]
-```
-
-`get_workflow_status` reads the legacy `/api/workflows/{id}/status` route. `validate_workflow` returns only its `is_deployed` flag; it returns `False` if the status request raises `SimStudioError`. It does not validate credentials, input, quota, or deployment readiness. Use the status method directly to distinguish a failed lookup from an undeployed workflow:
-
-```python
-status = client.get_workflow_status("workflow-id")
-print(status.is_deployed, status.needs_redeployment)
-```
-
-`get_workflow_run` reads and unwraps the v2 run resource. `include_output=True` includes final output for a completed run; default omitted/false. Run-read `selected_outputs` uses **block IDs**, as `blockId` or `blockId.path`, rather than block names. Selected block outputs are independent of `include_output`.
-
-`get_job_status` is for IDs created by legacy `0.1.x` async execution at `/api/jobs/{jobId}`. Use `get_workflow_run` for runs started by `0.2.x`.
-
-### Read limits and update the client
-
-```text
-get_rate_limit_info() -> RateLimitInfo | None
-get_usage_limits() -> UsageLimits
-set_api_key(api_key: str) -> None
-set_base_url(base_url: str) -> None
-close() -> None
-```
-
-`get_rate_limit_info` reads cached headers from the most recent response that supplied rate information. It does not make a request. `get_usage_limits` calls the legacy account helper; see its shape below. Setters change subsequent requests. `close` closes the HTTP session; a context manager calls it automatically.
-
-## Return objects [#data-classes]
-
-### WorkflowExecutionResult
-
-| Field | Python type/default | Current behavior |
-| --- | --- | --- |
-| `success` | `bool`, required | `True` only for completed or paused |
-| `status` | `Optional[str] = None` | Server status: completed, failed, paused, or cancelled |
-| `output` | `Optional[Any] = None` | Workflow output, including any returned partial result |
-| `error` | `Optional[str] = None` | Failure message, when present |
-| `metadata` | `Optional[Dict[str, Any]] = None` | SDK-built `duration`, `runId`, `startTime`, `endTime`; duration is milliseconds |
-| `total_duration` | `Optional[float] = None` | Duration in milliseconds |
-| `logs`, `trace_spans` | `Optional[list] = None` | Compatibility fields; not populated by the v2 adapter |
-
-Nested oversized values may be `LargeValueRef` objects with `__simLargeValueRef`, `version`, `id`, `kind`, `size`, optional `key`, `executionId`, and `preview`. Their keys are opaque server pointers, not download URLs. See [large outputs](/workflows/deployment/api#oversized-outputs).
-
-### AsyncExecutionResult
-
-| Field | Python type/default |
-| --- | --- |
-| `success` | `bool`, required; current accepted receipts set `True` |
-| `run_id` | `str`, required |
-| `status_url` | `str`, required |
-| `message` | `str = ""`; current adapter sets `"Workflow execution queued"` |
-| `async_execution` | `bool = True` |
-
-### WorkflowStatus
-
-| Field | Python type/default |
-| --- | --- |
-| `is_deployed` | `bool`, required |
-| `deployed_at` | `Optional[str] = None` |
-| `needs_redeployment` | `bool = False` |
-
-### Run dictionary
-
-`get_workflow_run` returns the server's `data` dictionary without changing camelCase keys:
-
-| Field | Shape |
-| --- | --- |
-| `runId`, `workflowId` | String identifiers |
-| `status` | `queued`, `pending`, `running`, `redacting`, `paused`, `completed`, `failed`, or `cancelled` |
-| `trigger` | String or `None` |
-| `startedAt`, `endedAt` | ISO timestamps or `None`; queued runs report enqueue time as startedAt |
-| `durationMs` | Number or `None` |
-| `paused` | Pause details or `None`; use `pauseKind` and `resumeAt` to identify the resume condition |
-| `cost` | `{"total": number}` in credits, or `None` when unavailable/withheld |
-| `error` | Structured `code`/`message` failure, or `None` |
-| `output` | Final output when requested for a completed run, otherwise `None` |
-| `blockOutputs` | Selected block-ID/path outputs, or `None` |
-| `files` | Produced-file metadata when output is requested/available, otherwise `None` |
-
-Each file includes `id`, `name`, byte `size`, MIME `type`, `downloadPath`, and nullable `base64`. The SDK does not expose file-inlining options; download at the returned path with an authenticated raw HTTP request. Permission policies can withhold cost or recorded output fields. See [Get workflow run](/api-reference/workflow-runs/getWorkflowRunV2) for the full contract, functional-output permission conflict, and file limits.
-
-### RateLimitInfo
-
-Fields are `limit: int`, `remaining: int`, `reset: int`, and `retry_after: Optional[int] = None`. V2 ISO reset headers become epoch **milliseconds**. Integer reset headers from older endpoints retain their original epoch-seconds value. For a v2 response:
-
-```python
-from datetime import datetime, timezone
-
-rate = client.get_rate_limit_info()
-if rate:
- print(rate.remaining)
- print(datetime.fromtimestamp(rate.reset / 1000, tz=timezone.utc))
-```
-
-Use a known response surface when converting `reset`; use `resetAt` ISO strings from the usage helper when you need a format that is unambiguous across endpoints.
-
-### UsageLimits
-
-The dataclass contains `success: bool`, `rate_limit: Dict[str, Any]`, and `usage: Dict[str, Any]`:
-
-```text
-rate_limit:
- sync / async:
- isLimited: bool
- requestsPerMinute: number
- maxBurst: number
- remaining: number
- resetAt: ISO timestamp
- authType: "api" or "manual"
-usage:
- currentPeriodCost: number (USD)
- limit: number (USD)
- plan: str
-```
-
-`requestsPerMinute` is the refill rate; `maxBurst` is the bucket capacity. There is no `rate_limit["sync"]["limit"]` field. The Python dataclass does not expose the legacy route's `storage` result.
-
-```python
-limits = client.get_usage_limits()
-print("Sync remaining:", limits.rate_limit["sync"]["remaining"])
-print("Resets at:", limits.rate_limit["sync"]["resetAt"])
-print("Period cost in USD:", limits.usage["currentPeriodCost"])
-```
-
-This helper resolves account/key-owner usage and can differ from the payer for a selected workspace. For workspace credit and storage state, use [Get billing status](/api-reference/billing/getBillingStatus). Logs APIs also report USD; v2 Billing and run-resource amounts use credits.
-
-## File inputs
-
-Include a binary file-like object under the input field your deployed trigger expects:
-
-```python
-import os
-from simstudio import SimStudioClient
-
-with SimStudioClient(api_key=os.environ["SIM_API_KEY"]) as client:
- with open("document.pdf", "rb") as document:
- result = client.execute_workflow_sync(
- os.environ["WORKFLOW_ID"],
- input={"documents": [document], "query": "Summarize this document"},
- )
- print(result.output)
-```
-
-The SDK reads and base64-encodes the bytes, retaining the file name and using its `content_type` attribute when available. Otherwise MIME type defaults to `application/octet-stream`. Validate upload size and content on your backend; the execute request has a 10 MiB JSON-body limit, including base64 overhead.
-
-## Execute several workflows [#batch-workflow-execution]
-
-The client has no batch-execution method. A sequential loop makes one request per workflow and handles each result independently:
-
-```python
-for workflow_id, workflow_input in [
- ("workflow-1", {"message": "Analyze this"}),
- ("workflow-2", {"message": "Summarize this"}),
-]:
- try:
- result = client.execute_workflow_sync(workflow_id, workflow_input)
- print(workflow_id, result.status, result.output, result.error)
- except SimStudioError as error:
- print(workflow_id, error.code, str(error))
-```
-
-Use this inside the configured client context. A deployment-status precheck is optional and cannot guarantee the later run will succeed.
-
-## Stream through HTTP [#streaming-workflow-execution]
-
-The current `0.2.x` SDK sends `stream=True` but then parses the response as JSON. It does not expose an SSE iterator; a valid SSE response fails that parser. Use raw HTTP for streaming:
-
-```python
-import os
-import requests
-
-workflow_id = os.environ["WORKFLOW_ID"]
-with requests.post(
- f"https://www.sim.ai/api/v2/workflows/{workflow_id}/execute",
- headers={
- "X-API-Key": os.environ["SIM_API_KEY"],
- "X-Sim-Stream-Protocol": "agent-events-v1",
- },
- json={"input": {"message": "Count to five"}, "stream": True,
- "selectedOutputs": ["agent.content"]},
- stream=True,
- timeout=(10, 300),
-) as response:
- response.raise_for_status()
- for line in response.iter_lines():
- if line:
- print(line.decode("utf-8"))
-```
-
-This prints raw SSE. Applications must parse complete events: success ends with `{event:"final",data:...}`, failure ends with `{event:"error",...}`, then `[DONE]`. Preserve structured output and reset events; do not filter only for `chunk` or the obsolete `done` event. The protocol header enables live answer tokens and requires handling `chunk_reset`; reasoning/tool events require their separate flags.
-
-For Flask or another web backend, authenticate the caller and authorize the workflow before opening the upstream request. Forward all SSE bytes with `text/event-stream`, check upstream HTTP status before starting the downstream response, and close the upstream response if the caller disconnects. See [API streaming](/workflows/deployment/api#streaming) and the [reference client](/workflows/deployment/agent-events#reference-client) for framing and event handling.
-
-## Upgrade from 0.1.x
-
-`0.2.0` changed execution requests to `/api/v2`, nests named input under `input`, and sends async/timeout options in the body instead of `X-Execution-Mode` and `X-Execution-Timeout-Seconds` headers.
-
-Replace `AsyncExecutionResult.job_id` with `run_id`; its old `execution_id` field is removed. Poll with `get_workflow_run(workflow_id, run_id)`, not `get_job_status`. Synchronous metadata is now SDK-built: use `metadata["runId"]` instead of the former `metadata["executionId"]`. Read `status` to distinguish failed and cancelled returns. Logs and trace spans are no longer populated by this adapter.
-
-Workflow status, legacy jobs, and account usage helpers still use their original routes. This SDK does not expose every v2 operation, manual execution, resume/cancel, caller-selected run headers, or NDJSON/agent-event streaming controls.
-
-## Related docs
-
-- [API authentication](/api-reference/authentication)
-- [TypeScript SDK](/api-reference/typescript)
-- [API getting started](/api-reference/getting-started)
-- [Get workflow run](/api-reference/workflow-runs/getWorkflowRunV2)
-- [Python package source and Apache-2.0 license](https://github.com/simstudioai/sim/tree/main/packages/python-sdk)
diff --git a/apps/docs/content/docs/api-reference/typescript.mdx b/apps/docs/content/docs/api-reference/typescript.mdx
deleted file mode 100644
index 77070283633..00000000000
--- a/apps/docs/content/docs/api-reference/typescript.mdx
+++ /dev/null
@@ -1,428 +0,0 @@
----
-title: TypeScript
-description: Execute deployed workflows from a server, poll runs, and handle SDK results.
----
-
-import { Callout } from '@/components/ui/callout'
-
-Use `SimStudioClient` from server-side JavaScript or TypeScript to execute deployed workflows. It includes TypeScript declarations, server-queued async execution, rate-limit retries, and file-input conversion. Browser applications should call an authenticated backend that keeps the Sim key private.
-
-## Install and configure [#installation]
-
-The ESM package requires Node.js `18+`:
-
-```bash
-bun add simstudio-ts-sdk
-```
-
-Create a key in **Account settings → Sim API keys**, or use a workspace key if your administrator requires one. Set `SIM_API_KEY` and `WORKFLOW_ID` in your server environment. Deploy the workflow you intend to call; SDK execution uses the active deployment. See [Authentication](/api-reference/authentication) for key types and permissions.
-
-### Server compatibility
-
-SDK `0.2.x` executes and polls through `/api/v2`. Your deployment must serve `POST /api/v2/workflows/{workflowId}/execute`; the client does not fall back when that route is absent. Upgrade an older self-hosted server, or intentionally retain a `0.1.x` SDK for legacy execution.
-
-The default `baseUrl` is `https://www.sim.ai`. Set it to your self-hosted deployment's origin. Versions before `0.2.1` defaulted to `https://sim.ai`; set the `www` origin explicitly when using an older SDK against Sim Cloud to avoid redirects.
-
-## Run a workflow [#quick-start]
-
-```typescript
-import { SimStudioClient, SimStudioError } from 'simstudio-ts-sdk'
-
-const apiKey = process.env.SIM_API_KEY
-const workflowId = process.env.WORKFLOW_ID
-if (!apiKey || !workflowId) throw new Error('Set SIM_API_KEY and WORKFLOW_ID')
-
-const client = new SimStudioClient({
- apiKey,
- baseUrl: process.env.SIM_BASE_URL || 'https://www.sim.ai',
-})
-
-try {
- const result = await client.executeWorkflowSync(workflowId, { message: 'Process this data' })
- const runId = result.metadata?.runId
- if (!result.success) {
- process.stdout.write('The workflow was cancelled.\n')
- } else if (runId) {
- const run = await client.getWorkflowRun(workflowId, runId, { includeOutput: true })
- process.stdout.write(JSON.stringify({ status: run.status, output: run.output, paused: run.paused }))
- }
-} catch (error) {
- if (error instanceof SimStudioError) {
- process.stderr.write(`${error.code}: ${error.message}\n`)
- }
- throw error
-}
-```
-
-Replace `message` with a field your deployed trigger accepts. A failed synchronous run rejects with `SimStudioError`, carrying the run's error code/message. A resolved `success:false` means cancellation. `success:true` means completed **or paused**, so it does not guarantee the workflow finished. The synchronous SDK result has no `status` field; use its `metadata.runId` with `getWorkflowRun` for the exact state.
-
-The Python SDK instead returns a failed synchronous result normally. Both SDKs throw for rejected HTTP requests and client/transport errors.
-
-## Start an asynchronous run [#async-workflow-execution]
-
-The receipt's `success:true` confirms acceptance, not completion. Poll the returned run ID:
-
-```typescript
-import { setTimeout as delay } from 'node:timers/promises'
-import { SimStudioClient } from 'simstudio-ts-sdk'
-
-const apiKey = process.env.SIM_API_KEY
-const workflowId = process.env.WORKFLOW_ID
-if (!apiKey || !workflowId) throw new Error('Set SIM_API_KEY and WORKFLOW_ID')
-const client = new SimStudioClient({ apiKey })
-const waiting = new Set(['queued', 'pending', 'running', 'redacting'])
-
-const receipt = await client.executeWorkflow(workflowId, { message: 'Process this dataset' }, {
- async: true,
- executionTimeoutSeconds: 3600,
-})
-if (!('async' in receipt) || !receipt.async) throw new Error('Expected an async run receipt')
-
-const deadline = Date.now() + 300_000
-let run = await client.getWorkflowRun(workflowId, receipt.runId, { includeOutput: true })
-while (waiting.has(run.status)) {
- if (Date.now() >= deadline) {
- throw new Error(`Stop waiting locally; run ${receipt.runId} may still be active`)
- }
- await delay(2000)
- run = await client.getWorkflowRun(workflowId, receipt.runId, { includeOutput: true })
-}
-process.stdout.write(JSON.stringify({ status: run.status, output: run.output, error: run.error, paused: run.paused }))
-```
-
-The five-minute polling deadline is an application choice and does not cancel the run. Handle `completed`, `failed`, and `cancelled` separately. Inspect a `paused` run's resume condition: time pauses can resume automatically; human pauses require input. Resume can create another attempt. See [Get workflow run](/api-reference/workflow-runs/getWorkflowRunV2) and [Resume workflow run](/api-reference/workflow-runs/resumeWorkflowRunV2).
-
-Async submission rejects `stream:true`, `selectedOutputs`, and other output-shaping options. `executionTimeoutSeconds` accepts an integer from `1` to `604800` and requires `async:true`. The effective cap is the smaller of this value and the account policy. Client `timeout` is separate and uses milliseconds.
-
-## Handle errors and retries [#error-handling]
-
-`new SimStudioError(message: string, code?: string, status?: number)` exposes a message, code, and HTTP status when available. An in-band failed run can throw without an HTTP error status.
-
-| Code | Meaning |
-| --- | --- |
-| `UNAUTHORIZED` | Missing, invalid, or expired credential |
-| `BAD_REQUEST` | V2 request validation failed |
-| `TIMEOUT` | The client stopped waiting |
-| `RATE_LIMIT_EXCEEDED` | SDK wrapper for HTTP `429` |
-| `USAGE_LIMIT_EXCEEDED` | A usage policy rejected the request or run |
-| `INVALID_EXECUTION_TIMEOUT` | An unsupported or invalid async timeout option |
-| `EXECUTION_ERROR` | Transport, response parsing, or execution-request failure |
-| `EXECUTION_FAILED` | Fallback code for a failed synchronous run |
-| `STATUS_ERROR`, `USAGE_ERROR` | A status or usage helper failed |
-
-Other server error codes pass through, including block/child execution failures. V2 reports malformed JSON/request input as `BAD_REQUEST`, rather than `INVALID_JSON`. See [API errors](/api-reference/getting-started#error-handling).
-
-`executeWithRetry` retries only rate-limit exceptions. By default it makes an initial attempt and up to three retries, using exponential backoff and ±25% jitter:
-
-```typescript
-const result = await client.executeWithRetry('workflow-id', { message: 'Process this' }, {
- timeout: 30000,
-}, {
- maxRetries: 3,
- initialDelay: 1000,
- maxDelay: 30000,
- backoffMultiplier: 2,
-})
-process.stdout.write(JSON.stringify(result))
-```
-
-All retry delays and `RateLimitInfo.retryAfter` use **milliseconds**. A supplied `Retry-After` takes precedence over calculated backoff; jitter still applies. `maxDelay` caps the calculated delay, not a server-supplied wait.
-
-
-A client timeout or lost response does not prove the run never started. The SDK's timeout stops waiting without aborting its fetch. Reconcile an uncertain execution before starting another run. Use raw v2 requests for caller-selected run IDs and [run-ID recovery](/api-reference/getting-started#sync-vs-async-execution).
-
-
-## Method reference [#api-reference]
-
-### Constructor
-
-```text
-new SimStudioClient(config: SimStudioConfig)
-```
-
-`SimStudioConfig` has required `apiKey:string` and optional `baseUrl:string`. Supply an origin without an `/api` suffix; the SDK removes trailing slashes and sends the key in `X-API-Key`.
-
-### Execute
-
-```text
-executeWorkflow(workflowId: string, input?: any, options?: ExecutionOptions)
- -> Promise
-executeWorkflowSync(workflowId: string, input?: any, options?: SyncExecutionOptions)
- -> Promise
-executeWithRetry(workflowId: string, input?: any, options?: ExecutionOptions,
- retryOptions?: RetryOptions) -> Promise
-```
-
-| `ExecutionOptions` field | Behavior and default |
-| --- | --- |
-| `timeout?:number` | Client wait in milliseconds; default `30000` |
-| `async?:boolean` | Default omitted/false; true queues the active deployment |
-| `executionTimeoutSeconds?:number` | Async-only integer cap, `1`–`604800`; cannot extend account policy |
-| `selectedOutputs?:string[]` | Normalized block-name/path selectors such as `agent.content`; incompatible with async submission |
-| `stream?:boolean` | Default omitted/false; the current SDK cannot parse SSE when true; use raw HTTP below |
-
-An object input becomes the v2 `input` object. A primitive or array is wrapped as `{input:value}` inside it; omitted/null input becomes `{}`. Nested `File` objects are converted when the runtime provides global `File`.
-
-`executeWorkflowSync` forces `async:false`. `SyncExecutionOptions` excludes `async` and `executionTimeoutSeconds`. `RetryOptions` has optional `maxRetries` (default `3`), `initialDelay` (`1000` ms), `maxDelay` (`30000` ms), and `backoffMultiplier` (`2`).
-
-The synchronous adapter does not expose v2 `blockOutputs`, even when `selectedOutputs` is supplied. Use a run read or raw HTTP when you need the selected-output map.
-
-### Read deployment and runs
-
-```text
-getWorkflowStatus(workflowId: string) -> Promise
-validateWorkflow(workflowId: string) -> Promise
-getWorkflowRun(workflowId: string, runId: string, options?: GetWorkflowRunOptions)
- -> Promise
-getJobStatus(taskId: string) -> Promise
-```
-
-`getWorkflowStatus` reads the legacy `/api/workflows/{id}/status` route. `validateWorkflow` returns only `isDeployed`, and returns false if the status lookup fails. It does not verify credentials, inputs, quotas, or deployment readiness. Use the status method directly to distinguish a failed lookup from an undeployed workflow:
-
-```typescript
-const deployment = await client.getWorkflowStatus('workflow-id')
-process.stdout.write(JSON.stringify(deployment))
-```
-
-`GetWorkflowRunOptions` contains `includeOutput?:boolean` and `selectedOutputs?:string[]`. Output is omitted/null by default; request it with `includeOutput:true` for a completed run. Run-read selectors use **block IDs**, as `blockId` or `blockId.path`, instead of block names. Selected block outputs are independent of `includeOutput`.
-
-`getJobStatus` polls IDs created by legacy `0.1.x` async execution at `/api/jobs/{taskId}`. Use `getWorkflowRun` for runs started by `0.2.x`.
-
-### Read limits and update the client
-
-```text
-getRateLimitInfo() -> RateLimitInfo | null
-getUsageLimits() -> Promise
-setApiKey(apiKey: string) -> void
-setBaseUrl(baseUrl: string) -> void
-```
-
-`getRateLimitInfo` reads cached headers from the most recent response that supplied rate information; it makes no request. `getUsageLimits` calls the legacy account helper. Setters affect subsequent requests.
-
-## Return types [#types]
-
-The SDK supplies declarations for these types. Arbitrary workflow input/output and log fields are not schema-typed by the client.
-
-### WorkflowExecutionResult
-
-| Field | Declared type | Current adapter behavior |
-| --- | --- | --- |
-| `success` | `boolean` | True for completed/paused; false for cancelled; failed rejects |
-| `executionId` | Optional `string` | V2 run ID |
-| `output` | Optional `any` | Workflow output |
-| `error` | Optional `string` | Returned message when present |
-| `metadata` | Optional object | `duration`, `executionId`, `runId`, `startTime`, `endTime`; permits other keys |
-| `totalDuration` | Optional `number` | Duration in milliseconds |
-| `logs`, `traceSpans` | Optional `any[]` | Compatibility fields; not populated by the v2 adapter |
-
-Metadata's duration is an optional number in milliseconds; identifiers/timestamps are optional strings. The current synchronous type has no `status` property.
-
-### AsyncExecutionResult
-
-Fields are `success:boolean`, `runId:string`, `statusUrl:string`, `message:string`, and `async:true`. The adapter sets `success:true` and `message:'Workflow execution queued'` for a `202` receipt.
-
-### WorkflowStatus
-
-Fields are `isDeployed:boolean`, optional `deployedAt:string`, `needsRedeployment:boolean`, and the optional compatibility field `isPublished:boolean`. The current status endpoint does not populate `isPublished`.
-
-### WorkflowRunStatus
-
-The server resource returned by `getWorkflowRun` includes:
-
-| Field | Shape |
-| --- | --- |
-| `runId`, `workflowId` | Strings |
-| `status` | queued, pending, running, redacting, paused, completed, failed, or cancelled |
-| `trigger` | String or null |
-| `startedAt`, `endedAt` | ISO timestamp or null; queued startedAt is enqueue time |
-| `durationMs` | Number or null |
-| `paused` | Pause-detail object or null; inspect `pauseKind` and `resumeAt` |
-| `cost` | `{total:number}` in credits, or null when unavailable/withheld |
-| `error` | `WorkflowExecutionError` with code/message and optional details, or null |
-| `output` | Requested completed-run output, otherwise null |
-| `blockOutputs` | Selected block-ID/path values, or null |
-| `files` | Produced-file metadata when output is requested/available, otherwise null |
-
-The `0.2.x` exported interface currently omits `redacting` and `files`, although the runtime returns the server object unchanged. The polling example uses a string set so it can wait through redaction without asserting an unsupported type. The interface declares `paused` and `blockOutputs` as nullable records, and the other fields with the nullability shown above.
-
-Files have `id`, `name`, byte `size`, MIME `type`, `downloadPath`, and nullable `base64`. The SDK exposes no file-inlining options; use the returned download path with an authenticated HTTP request. Permission policies can withhold costs or recorded outputs. See [Get workflow run](/api-reference/workflow-runs/getWorkflowRunV2) for the full contract, functional-output permission conflict, and file limits.
-
-### JobStatusResult
-
-The legacy type has `taskId:string`, `status:string`, optional `metadata:Record`, `output:unknown`, and `error:string`. It describes legacy job polling, not a v2 run receipt.
-
-### LargeValueRef
-
-Oversized nested output can contain a placeholder with required `__simLargeValueRef:true`, `version:1`, `id:string`, `kind:'array'|'object'|'string'|'json'`, and `size:number`. Optional fields are `key:string`, `executionId:string`, and `preview:unknown`. These storage keys are opaque server pointers, not download URLs. See [large outputs](/workflows/deployment/api#oversized-outputs).
-
-### RateLimitInfo
-
-Fields are `limit:number`, `remaining:number`, `reset:number`, and optional `retryAfter:number`. V2 ISO reset headers become epoch **milliseconds**; older integer headers retain their epoch-seconds value. For v2:
-
-```typescript
-const rate = client.getRateLimitInfo()
-if (rate) {
- process.stdout.write(`${rate.remaining} remaining; reset ${new Date(rate.reset).toISOString()}\n`)
-}
-```
-
-Use a known response surface when converting reset values. The usage helper's `resetAt` ISO strings avoid the mixed legacy units.
-
-### UsageLimits
-
-```text
-success: boolean
-rateLimit:
- sync / async:
- isLimited: boolean
- requestsPerMinute: number
- maxBurst: number
- remaining: number
- resetAt: string (ISO timestamp)
- authType: string (server values "api" or "manual")
-usage:
- currentPeriodCost: number (USD)
- limit: number (USD)
- plan: string
-storage:
- usedBytes: number
- limitBytes: number
- percentUsed: number
-```
-
-`requestsPerMinute` is the refill rate; `maxBurst` is the bucket capacity. There is no `rateLimit.sync.limit` field.
-
-```typescript
-const limits = await client.getUsageLimits()
-process.stdout.write(JSON.stringify({
- remaining: limits.rateLimit.sync.remaining,
- resetAt: limits.rateLimit.sync.resetAt,
- periodCostUSD: limits.usage.currentPeriodCost,
- storageBytes: limits.storage.usedBytes,
-}))
-```
-
-This legacy helper resolves account/key-owner usage and can differ from the selected workspace's payer. Use [Get billing status](/api-reference/billing/getBillingStatus) for workspace credits/storage. Logs APIs also report USD; v2 Billing and run-resource amounts use credits. If calculating a usage percentage, handle a zero limit before dividing.
-
-## File inputs [#file-upload]
-
-The SDK recursively converts global `File` objects to:
-
-```typescript
-const inlineFile = {
- type: 'file',
- data: 'data:application/pdf;base64,BASE64_BYTES',
- name: 'document.pdf',
- mime: 'application/pdf',
-}
-```
-
-You can also supply a URL file:
-
-```typescript
-const urlFile = {
- type: 'url',
- data: 'https://example.com/document.pdf',
- name: 'document.pdf',
- mime: 'application/pdf',
-}
-```
-
-Place files under the input field your deployed trigger expects. Run uploads on the backend after authenticating the caller and validating size/content. Base64 overhead counts toward the execute endpoint's 10 MiB JSON-body limit.
-
-For automatic conversion, this example requires **Node.js 20+**, where `File` is global:
-
-```typescript
-import { readFile } from 'node:fs/promises'
-import { SimStudioClient } from 'simstudio-ts-sdk'
-
-const apiKey = process.env.SIM_API_KEY
-const workflowId = process.env.WORKFLOW_ID
-if (!apiKey || !workflowId) throw new Error('Set SIM_API_KEY and WORKFLOW_ID')
-const client = new SimStudioClient({ apiKey })
-const bytes = await readFile('./document.pdf')
-const file = new File([new Uint8Array(bytes)], 'document.pdf', { type: 'application/pdf' })
-const result = await client.executeWorkflowSync(workflowId, {
- documents: [file],
- query: 'Summarize this document',
-})
-process.stdout.write(JSON.stringify(result.output))
-```
-
-On Node.js 18, use a serialized base64/URL object. Importing `File` from `node:buffer` only in the caller does not satisfy the SDK's global-File detection.
-
-## Use from a web backend [#browser-usage]
-
-Keep the key on the server. Authenticate the application caller, resolve a workflow they may run, validate the input, and only then call the SDK. A trusted server key does not authorize every browser caller to run every workflow.
-
-This helper is the SDK call to use after those checks:
-
-```typescript
-async function executeAuthorizedWorkflow(workflowId: string, input: Record) {
- return client.executeWorkflowSync(workflowId, input, { timeout: 60000 })
-}
-```
-
-### Express integration [#nodejs-express-integration]
-
-Call the helper from a POST handler after authentication and authorization middleware. Parse JSON with a request-size limit, map rejected SDK requests to an appropriate HTTP status, and preserve cancellation/paused state instead of wrapping every resolved result as completed success.
-
-### Next.js API route
-
-Call the same helper from an authenticated server route. In a Pages Router API handler, reject methods other than POST with `405`; in either router, authorize workflow selection before execution. Set the server's `SIM_API_KEY` environment variable and keep it out of browser bundles.
-
-## Stream through HTTP [#streaming-workflow-execution]
-
-The current `0.2.x` SDK sends `stream:true` but unconditionally parses JSON. It has no SSE iterator/callback and cannot consume a valid SSE response. Use raw HTTP instead:
-
-```typescript
-const apiKey = process.env.SIM_API_KEY
-const workflowId = process.env.WORKFLOW_ID
-if (!apiKey || !workflowId) throw new Error('Set SIM_API_KEY and WORKFLOW_ID')
-
-const response = await fetch(`https://www.sim.ai/api/v2/workflows/${workflowId}/execute`, {
- method: 'POST',
- headers: {
- 'X-API-Key': apiKey,
- 'Content-Type': 'application/json',
- 'X-Sim-Stream-Protocol': 'agent-events-v1',
- },
- body: JSON.stringify({ input: { message: 'Count to five' }, stream: true,
- selectedOutputs: ['agent.content'] }),
-})
-if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`)
-if (!response.body) throw new Error('Missing SSE response body')
-
-const decoder = new TextDecoder()
-const reader = response.body.getReader()
-try {
- while (true) {
- const { done, value } = await reader.read()
- if (done) break
- process.stdout.write(decoder.decode(value, { stream: true }))
- }
- process.stdout.write(decoder.decode())
-} finally {
- reader.releaseLock()
-}
-```
-
-This prints raw SSE; network chunks are not necessarily complete events. Success emits `{event:"final",data:...}`, failure emits `{event:"error",...}`, then `[DONE]`. Preserve output and reset events. The protocol header enables live answer tokens and requires `chunk_reset` handling; thinking/tool events require their separate flags. For browsers, have the authenticated backend forward the upstream stream, check HTTP status, and close/cancel it on disconnect.
-
-See [API streaming](/workflows/deployment/api#streaming) and the [reference client](/workflows/deployment/agent-events#reference-client) for event parsing and protocol details. Synchronous NDJSON heartbeats are a separate transport selected by `Accept: application/x-ndjson`; the SDK has no option for that header.
-
-## Upgrade from 0.1.x
-
-`0.2.0` changed execution to `/api/v2`, nests named input under `input`, and puts async/timeout options in the body instead of `X-Execution-Mode` and `X-Execution-Timeout-Seconds` headers.
-
-Replace `AsyncExecutionResult.jobId` with `runId`; its old `executionId` is removed. Poll with `getWorkflowRun(workflowId, runId)`, not `getJobStatus`. Failed synchronous runs now reject; move failure handling from a `!result.success` branch into `catch`. A resolved false result means cancellation; true also includes paused runs.
-
-Workflow status, legacy jobs, and account usage helpers still use their original routes. This SDK does not expose every v2 operation, manual draft execution, resume/cancel, caller-selected run headers, or NDJSON/agent-event streaming controls.
-
-## Related docs
-
-- [API authentication](/api-reference/authentication)
-- [Python SDK](/api-reference/python)
-- [API getting started](/api-reference/getting-started)
-- [Get workflow run](/api-reference/workflow-runs/getWorkflowRunV2)
-- [TypeScript package source and Apache-2.0 license](https://github.com/simstudioai/sim/tree/main/packages/ts-sdk)
diff --git a/apps/docs/content/docs/cli/index.mdx b/apps/docs/content/docs/cli/index.mdx
index 0bd341747a7..b701acd5cae 100644
--- a/apps/docs/content/docs/cli/index.mdx
+++ b/apps/docs/content/docs/cli/index.mdx
@@ -183,4 +183,4 @@ The skills assume `sim` is installed and signed in, so complete [Your first comm
- [Scripting](/cli/scripting)
- [Workflow imports and workspace sync](/cli/workflow-sync)
- [Troubleshooting](/cli/troubleshooting)
-- [TypeScript SDK](/api-reference/typescript), [Python SDK](/api-reference/python), and [HTTP API](/api-reference/getting-started)
+- [HTTP API](/api-reference/getting-started)
diff --git a/apps/docs/lib/redirects.ts b/apps/docs/lib/redirects.ts
index a0ea0a5cb06..51b214455ca 100644
--- a/apps/docs/lib/redirects.ts
+++ b/apps/docs/lib/redirects.ts
@@ -93,8 +93,19 @@ const PATH_REDIRECTS: DocsRedirect[] = [
{ source: '/api-reference', destination: '/api-reference/getting-started', permanent: true },
// sdks/* folded into api-reference/
{ source: '/sdks', destination: '/api-reference/getting-started', permanent: true },
- { source: '/sdks/typescript', destination: '/api-reference/typescript', permanent: true },
- { source: '/sdks/python', destination: '/api-reference/python', permanent: true },
+ { source: '/sdks/typescript', destination: '/api-reference/getting-started', permanent: true },
+ { source: '/sdks/python', destination: '/api-reference/getting-started', permanent: true },
+ // TypeScript and Python SDKs removed; their pages point at the HTTP API quickstart
+ {
+ source: '/api-reference/typescript',
+ destination: '/api-reference/getting-started',
+ permanent: true,
+ },
+ {
+ source: '/api-reference/python',
+ destination: '/api-reference/getting-started',
+ permanent: true,
+ },
// building-agents/agents merged into the building-agents overview
{ source: '/building-agents/agents', destination: '/agents', permanent: true },
// form deployment removed
diff --git a/apps/docs/scripts/fixtures/content-baseline.json b/apps/docs/scripts/fixtures/content-baseline.json
index 9506dc1385c..b192484a92d 100644
--- a/apps/docs/scripts/fixtures/content-baseline.json
+++ b/apps/docs/scripts/fixtures/content-baseline.json
@@ -648,15 +648,11 @@
"/platform/costs",
"#oauth-access-tokens",
"/api-reference/getting-started",
- "/api-reference/python",
- "/api-reference/typescript",
"/cli/authentication",
"/platform/self-hosting/environment-variables",
"/api-reference/getting-started",
"/cli/authentication",
- "/platform/permissions",
- "/api-reference/python",
- "/api-reference/typescript"
+ "/platform/permissions"
],
"media": []
},
@@ -904,8 +900,6 @@
],
"links": [
"/openapi.json",
- "/api-reference/typescript",
- "/api-reference/python",
"/cli",
"/api-reference/authentication",
"/api-reference/workflows/listWorkflows",
@@ -916,585 +910,11 @@
"/workflows/deployment/api#streaming",
"/api-reference/billing/getBillingStatus",
"/api-reference/authentication",
- "/api-reference/python",
- "/api-reference/typescript",
"/api-reference/workflows/executeWorkflowV2",
"/api-reference/workflow-sync"
],
"media": []
},
- {
- "source": "api-reference/python.mdx",
- "url": "/api-reference/python",
- "sha256": "f6fdd02aa4a75ffe368e50de6a4d7c26302801af897a1a25263a26fc41e869ad",
- "bytes": 20330,
- "frontmatter": {
- "title": "Python",
- "description": "Execute deployed workflows from Python, poll runs, and handle SDK results."
- },
- "headings": [
- {
- "title": "Install and configure",
- "url": "#installation",
- "depth": 2
- },
- {
- "title": "Server compatibility",
- "url": "#server-compatibility",
- "depth": 3
- },
- {
- "title": "Run a workflow",
- "url": "#quick-start",
- "depth": 2
- },
- {
- "title": "Start an asynchronous run",
- "url": "#async-workflow-execution",
- "depth": 2
- },
- {
- "title": "Handle errors and rate limits",
- "url": "#error-handling",
- "depth": 2
- },
- {
- "title": "Method reference",
- "url": "#api-reference",
- "depth": 2
- },
- {
- "title": "Constructor",
- "url": "#constructor",
- "depth": 3
- },
- {
- "title": "Execute",
- "url": "#execute",
- "depth": 3
- },
- {
- "title": "Read workflow deployment and runs",
- "url": "#read-workflow-deployment-and-runs",
- "depth": 3
- },
- {
- "title": "Read limits and update the client",
- "url": "#read-limits-and-update-the-client",
- "depth": 3
- },
- {
- "title": "Return objects",
- "url": "#data-classes",
- "depth": 2
- },
- {
- "title": "WorkflowExecutionResult",
- "url": "#workflowexecutionresult",
- "depth": 3
- },
- {
- "title": "AsyncExecutionResult",
- "url": "#asyncexecutionresult",
- "depth": 3
- },
- {
- "title": "WorkflowStatus",
- "url": "#workflowstatus",
- "depth": 3
- },
- {
- "title": "Run dictionary",
- "url": "#run-dictionary",
- "depth": 3
- },
- {
- "title": "RateLimitInfo",
- "url": "#ratelimitinfo",
- "depth": 3
- },
- {
- "title": "UsageLimits",
- "url": "#usagelimits",
- "depth": 3
- },
- {
- "title": "File inputs",
- "url": "#file-inputs",
- "depth": 2
- },
- {
- "title": "Execute several workflows",
- "url": "#batch-workflow-execution",
- "depth": 2
- },
- {
- "title": "Stream through HTTP",
- "url": "#streaming-workflow-execution",
- "depth": 2
- },
- {
- "title": "Upgrade from 0.1.x",
- "url": "#upgrade-from-01x",
- "depth": 2
- },
- {
- "title": "Related docs",
- "url": "#related-docs",
- "depth": 2
- }
- ],
- "explicitIds": [],
- "imports": [
- {
- "source": "fumadocs-ui/components/callout",
- "names": [
- {
- "local": "Callout",
- "imported": "Callout"
- }
- ]
- }
- ],
- "components": [
- {
- "name": "Callout",
- "attributes": {
- "type": "warn"
- },
- "line": 137
- }
- ],
- "expressions": [],
- "codeFences": [
- {
- "lang": "bash",
- "meta": null,
- "sha256": "4b7a6f45be2afc4518f3a3339c7baee59d88b0a655b96e3da44bfbd8ac95be3e",
- "bytes": 25,
- "line": 10
- },
- {
- "lang": "python",
- "meta": null,
- "sha256": "eb1a4a2d1885ba1013feb4097beae8f667870d1d2222ff2e391d7c03a798ce65",
- "bytes": 776,
- "line": 26
- },
- {
- "lang": "python",
- "meta": null,
- "sha256": "c1677ae09cc712a7f53ad7abb04b64d2c905deca70ab5d2571ee195635b01c28",
- "bytes": 1119,
- "line": 60
- },
- {
- "lang": "python",
- "meta": null,
- "sha256": "1202876d901ed0abbf20cc69d0ac3ee05ab900395fc5a491fdc3f69e17f65492",
- "bytes": 385,
- "line": 118
- },
- {
- "lang": "text",
- "meta": null,
- "sha256": "46d4f38bdd673c87afe3e8b4832faa98226e07d23344b9385ba04a7658047a5d",
- "bytes": 67,
- "line": 145
- },
- {
- "lang": "text",
- "meta": null,
- "sha256": "340adca5c4921786619fd14319bf0498b6e25961eba5b82e91a5595f83f3e6a3",
- "bytes": 645,
- "line": 153
- },
- {
- "lang": "text",
- "meta": null,
- "sha256": "6be2019327342bcddbae35924c8e7f00236552ca4a3b8bf44880ae47c5e157b0",
- "bytes": 262,
- "line": 187
- },
- {
- "lang": "python",
- "meta": null,
- "sha256": "7d50e2341d850611f67296e8ed7fb2a63f3b79a02627c8cfec1f29ec02be2d10",
- "bytes": 103,
- "line": 197
- },
- {
- "lang": "text",
- "meta": null,
- "sha256": "93613572aa1c2d5e83ff53b54dbd4f00d389c7e9163f9bb7d5c7b22fa085e55e",
- "bytes": 165,
- "line": 208
- },
- {
- "lang": "python",
- "meta": null,
- "sha256": "a0173b58cee71b947bc2acb4437c6a7a6236b27af78436b7d2d9bd731ac4742c",
- "bytes": 181,
- "line": 276
- },
- {
- "lang": "text",
- "meta": null,
- "sha256": "9afcfcab96aabedc5778d0e629ec1836a52741196772f7a36575bbe6b4368ecb",
- "bytes": 252,
- "line": 291
- },
- {
- "lang": "python",
- "meta": null,
- "sha256": "c54b0954c8c12ba9e3d97566ee379c55e1620206faac796ad9fc16db6c8d1d34",
- "bytes": 221,
- "line": 308
- },
- {
- "lang": "python",
- "meta": null,
- "sha256": "27f4dad755ed9478fa1cebc32bf3a98df7a560d5eafc3b8aa5ad801aa6539504",
- "bytes": 366,
- "line": 321
- },
- {
- "lang": "python",
- "meta": null,
- "sha256": "6be28d212d424441cfca53571a5470674b9d235ae19d5e3618edc902aa2f2b82",
- "bytes": 381,
- "line": 340
- },
- {
- "lang": "python",
- "meta": null,
- "sha256": "c98622c3e36c7d24f58009561ed68a8851792efeb85fe826e7b11ed18d8438c0",
- "bytes": 570,
- "line": 358
- }
- ],
- "links": [
- "/api-reference/authentication",
- "/api-reference/workflow-runs/getWorkflowRunV2",
- "/api-reference/workflow-runs/resumeWorkflowRunV2",
- "/api-reference/getting-started#error-handling",
- "/api-reference/getting-started#sync-vs-async-execution",
- "/workflows/deployment/api#oversized-outputs",
- "/api-reference/workflow-runs/getWorkflowRunV2",
- "/api-reference/billing/getBillingStatus",
- "/workflows/deployment/api#streaming",
- "/workflows/deployment/agent-events#reference-client",
- "/api-reference/authentication",
- "/api-reference/typescript",
- "/api-reference/getting-started",
- "/api-reference/workflow-runs/getWorkflowRunV2",
- "https://github.com/simstudioai/sim/tree/main/packages/python-sdk"
- ],
- "media": []
- },
- {
- "source": "api-reference/typescript.mdx",
- "url": "/api-reference/typescript",
- "sha256": "48c4610c3e883157facf2464d234977fdcc3400d910681c8dd13534bd20f806d",
- "bytes": 22085,
- "frontmatter": {
- "title": "TypeScript",
- "description": "Execute deployed workflows from a server, poll runs, and handle SDK results."
- },
- "headings": [
- {
- "title": "Install and configure",
- "url": "#installation",
- "depth": 2
- },
- {
- "title": "Server compatibility",
- "url": "#server-compatibility",
- "depth": 3
- },
- {
- "title": "Run a workflow",
- "url": "#quick-start",
- "depth": 2
- },
- {
- "title": "Start an asynchronous run",
- "url": "#async-workflow-execution",
- "depth": 2
- },
- {
- "title": "Handle errors and retries",
- "url": "#error-handling",
- "depth": 2
- },
- {
- "title": "Method reference",
- "url": "#api-reference",
- "depth": 2
- },
- {
- "title": "Constructor",
- "url": "#constructor",
- "depth": 3
- },
- {
- "title": "Execute",
- "url": "#execute",
- "depth": 3
- },
- {
- "title": "Read deployment and runs",
- "url": "#read-deployment-and-runs",
- "depth": 3
- },
- {
- "title": "Read limits and update the client",
- "url": "#read-limits-and-update-the-client",
- "depth": 3
- },
- {
- "title": "Return types",
- "url": "#types",
- "depth": 2
- },
- {
- "title": "WorkflowExecutionResult",
- "url": "#workflowexecutionresult",
- "depth": 3
- },
- {
- "title": "AsyncExecutionResult",
- "url": "#asyncexecutionresult",
- "depth": 3
- },
- {
- "title": "WorkflowStatus",
- "url": "#workflowstatus",
- "depth": 3
- },
- {
- "title": "WorkflowRunStatus",
- "url": "#workflowrunstatus",
- "depth": 3
- },
- {
- "title": "JobStatusResult",
- "url": "#jobstatusresult",
- "depth": 3
- },
- {
- "title": "LargeValueRef",
- "url": "#largevalueref",
- "depth": 3
- },
- {
- "title": "RateLimitInfo",
- "url": "#ratelimitinfo",
- "depth": 3
- },
- {
- "title": "UsageLimits",
- "url": "#usagelimits",
- "depth": 3
- },
- {
- "title": "File inputs",
- "url": "#file-upload",
- "depth": 2
- },
- {
- "title": "Use from a web backend",
- "url": "#browser-usage",
- "depth": 2
- },
- {
- "title": "Express integration",
- "url": "#nodejs-express-integration",
- "depth": 3
- },
- {
- "title": "Next.js API route",
- "url": "#nextjs-api-route",
- "depth": 3
- },
- {
- "title": "Stream through HTTP",
- "url": "#streaming-workflow-execution",
- "depth": 2
- },
- {
- "title": "Upgrade from 0.1.x",
- "url": "#upgrade-from-01x",
- "depth": 2
- },
- {
- "title": "Related docs",
- "url": "#related-docs",
- "depth": 2
- }
- ],
- "explicitIds": [],
- "imports": [
- {
- "source": "fumadocs-ui/components/callout",
- "names": [
- {
- "local": "Callout",
- "imported": "Callout"
- }
- ]
- }
- ],
- "components": [
- {
- "name": "Callout",
- "attributes": {
- "type": "warn"
- },
- "line": 127
- }
- ],
- "expressions": [],
- "codeFences": [
- {
- "lang": "bash",
- "meta": null,
- "sha256": "a3cd775cbb4aa2b9f93b0b048abc680f916c675c3090fa6d20a479a526f9e456",
- "bytes": 24,
- "line": 10
- },
- {
- "lang": "typescript",
- "meta": null,
- "sha256": "76a9e38658aa530d71622cd2bcfdca46679fcd77e11e5daf5e4d22fa16477c90",
- "bytes": 925,
- "line": 24
- },
- {
- "lang": "typescript",
- "meta": null,
- "sha256": "494e2b1fff12dc593c20c12f14acfbb862ce4f3793806d5e227fcea74f3d5cbb",
- "bytes": 1153,
- "line": 61
- },
- {
- "lang": "typescript",
- "meta": null,
- "sha256": "2439dcdf61021131759b26013a22208cad2986d48137bb2270f74a82b15c2b2a",
- "bytes": 243,
- "line": 113
- },
- {
- "lang": "text",
- "meta": null,
- "sha256": "c2167de31fcd5b335ea7afa84f45e3be314bee52b29de78239ff3bda981c104d",
- "bytes": 44,
- "line": 135
- },
- {
- "lang": "text",
- "meta": null,
- "sha256": "f8a8c3d343140543b5b2330c2e8721c846e05a2ebba473ef2db1fc77986f64ca",
- "bytes": 428,
- "line": 143
- },
- {
- "lang": "text",
- "meta": null,
- "sha256": "e73d36e02d17bb95dce1e7a60f7a2c8b1109bf96f72ad249959a33f2734f4d21",
- "bytes": 293,
- "line": 168
- },
- {
- "lang": "typescript",
- "meta": null,
- "sha256": "811d8583f3d1f4655b4c70914b229fce499c7002c17e1856dc12567d29ff4439",
- "bytes": 113,
- "line": 178
- },
- {
- "lang": "text",
- "meta": null,
- "sha256": "cb34f55a09b67eb5cf3faf9d34cd8b63f1438b19106f616d308ca33a8854e090",
- "bytes": 153,
- "line": 189
- },
- {
- "lang": "typescript",
- "meta": null,
- "sha256": "ef2e937be62740a50aeb99c136cb8afa24298ea28810baf04a84874b7e66248c",
- "bytes": 153,
- "line": 258
- },
- {
- "lang": "text",
- "meta": null,
- "sha256": "2b44123a52d64b62c138532058bc04ce2ecb964f9a75ec4f12298af49fb35794",
- "bytes": 378,
- "line": 269
- },
- {
- "lang": "typescript",
- "meta": null,
- "sha256": "6fca0e1fe5c65eff8153bfa027319556df2ccbcfd4b0a1eff06c72ea6fd02f43",
- "bytes": 265,
- "line": 291
- },
- {
- "lang": "typescript",
- "meta": null,
- "sha256": "f59b121b324660695e548c3d13bd0e572b623f4b79e24d6f1317c5b125119c96",
- "bytes": 141,
- "line": 307
- },
- {
- "lang": "typescript",
- "meta": null,
- "sha256": "3f33d121a0e6bcec039e6c20490cb1641bf21b8196731825ef9625e2de749424",
- "bytes": 129,
- "line": 318
- },
- {
- "lang": "typescript",
- "meta": null,
- "sha256": "a5c27589fcc685ba165109d7cd983d306049dcd3753d95a747decd6401e763c1",
- "bytes": 616,
- "line": 331
- },
- {
- "lang": "typescript",
- "meta": null,
- "sha256": "e5e40b323691e88e6b2e4632e58e4e515b7e8869cb75976d63c274a96f05352c",
- "bytes": 171,
- "line": 356
- },
- {
- "lang": "typescript",
- "meta": null,
- "sha256": "66957c241bc7f84b68bfd41dc3b1270c1e3ecd2d8b5acd27fde2f25c3b3763fb",
- "bytes": 991,
- "line": 374
- }
- ],
- "links": [
- "/api-reference/authentication",
- "/api-reference/workflow-runs/getWorkflowRunV2",
- "/api-reference/workflow-runs/resumeWorkflowRunV2",
- "/api-reference/getting-started#error-handling",
- "/api-reference/getting-started#sync-vs-async-execution",
- "/api-reference/workflow-runs/getWorkflowRunV2",
- "/workflows/deployment/api#oversized-outputs",
- "/api-reference/billing/getBillingStatus",
- "/workflows/deployment/api#streaming",
- "/workflows/deployment/agent-events#reference-client",
- "/api-reference/authentication",
- "/api-reference/python",
- "/api-reference/getting-started",
- "/api-reference/workflow-runs/getWorkflowRunV2",
- "https://github.com/simstudioai/sim/tree/main/packages/ts-sdk"
- ],
- "media": []
- },
{
"source": "api-reference/workflow-sync.mdx",
"url": "/api-reference/workflow-sync",
@@ -4739,8 +4159,6 @@
"/cli/scripting",
"/cli/workflow-sync",
"/cli/troubleshooting",
- "/api-reference/typescript",
- "/api-reference/python",
"/api-reference/getting-started"
],
"media": []
@@ -233569,12 +232987,12 @@
},
{
"source": "/sdks/typescript",
- "destination": "/api-reference/typescript",
+ "destination": "/api-reference/getting-started",
"permanent": true
},
{
"source": "/sdks/python",
- "destination": "/api-reference/python",
+ "destination": "/api-reference/getting-started",
"permanent": true
},
{
@@ -234189,12 +233607,12 @@
},
{
"source": "/:lang(en|es|fr|de|ja|zh)/sdks/typescript",
- "destination": "/api-reference/typescript",
+ "destination": "/api-reference/getting-started",
"permanent": true
},
{
"source": "/:lang(en|es|fr|de|ja|zh)/sdks/python",
- "destination": "/api-reference/python",
+ "destination": "/api-reference/getting-started",
"permanent": true
},
{
diff --git a/apps/sim/app/index.md/route.ts b/apps/sim/app/index.md/route.ts
index aa23f7cd198..51749fc2be6 100644
--- a/apps/sim/app/index.md/route.ts
+++ b/apps/sim/app/index.md/route.ts
@@ -46,12 +46,6 @@ export async function GET() {
`${DOCS_URL}/api-reference/getting-started`,
'Create an API key and run a deployed workflow'
),
- linkLine(
- 'TypeScript SDK',
- `${DOCS_URL}/api-reference/typescript`,
- '`bun add simstudio-ts-sdk`'
- ),
- linkLine('Python SDK', `${DOCS_URL}/api-reference/python`, '`pip install simstudio-sdk`'),
linkLine('CLI', `${DOCS_URL}/cli`, '`bun add -g sim`'),
linkLine('MCP server', `${DOCS_URL}/mcp/overview`, 'Connect an AI app to Sim'),
]),
diff --git a/apps/sim/content/library/how-to-create-an-ai-agent/index.mdx b/apps/sim/content/library/how-to-create-an-ai-agent/index.mdx
index c3736ddc8c0..7db1c257d9f 100644
--- a/apps/sim/content/library/how-to-create-an-ai-agent/index.mdx
+++ b/apps/sim/content/library/how-to-create-an-ai-agent/index.mdx
@@ -20,7 +20,7 @@ faq:
- q: "How long does it take to build an AI agent with Sim?"
a: "A simple agent, like the people research example in Sim's getting-started tutorial, takes about 10 minutes from account creation to a working deployment. Production-grade agents with multiple tools, conditional logic, and error handling take longer, and the visual builder keeps the feedback loop short: edit, run, read the trace, and refine."
- q: "Do I need to know how to code to build an AI agent?"
- a: "No. Sim's visual builder and Chat let you create, configure, and deploy agents entirely without code. You drag blocks, write system prompts in plain English, and connect integrations through the UI. That said, Sim also supports custom functions, a Python SDK, and full API access for developers who want deeper control or need to embed agents into existing codebases. Custom APIs, interfaces, and production infrastructure may still require technical work."
+ a: "No. Sim's visual builder and Chat let you create, configure, and deploy agents entirely without code. You drag blocks, write system prompts in plain English, and connect integrations through the UI. That said, Sim also supports custom functions, a CLI, and full API access for developers who want deeper control or need to embed agents into existing codebases. Custom APIs, interfaces, and production infrastructure may still require technical work."
- q: "How do I create an AI agent without much technical experience?"
a: "Sim lets people with limited technical experience create an AI agent by breaking one task into visible steps, starting from examples, connecting existing integrations, requiring structured output, and testing each route before deployment. A narrow, reversible first task keeps mistakes easy to catch."
- q: "What skills do I need to build an AI agent?"
diff --git a/apps/sim/lib/compare/data/sim.ts b/apps/sim/lib/compare/data/sim.ts
index 1c9492af710..4a5f189c699 100644
--- a/apps/sim/lib/compare/data/sim.ts
+++ b/apps/sim/lib/compare/data/sim.ts
@@ -730,19 +730,19 @@ export const simProfile: CompetitorProfile = {
},
extensibilitySdk: {
value:
- 'Official client SDKs exist for Python (simstudio-sdk, pip install simstudio-sdk) and TypeScript/JavaScript (simstudio-ts-sdk, npm install simstudio-ts-sdk), both wrapping the REST API (x-api-key header) with methods like execute_workflow / executeWorkflow, retry-with-backoff, and usage-limit lookups. Extensibility is further extended by MCP (client + server), a sandboxed code-execution block (JS/Python), custom tools, and an Agent-to-Agent (A2A) protocol block for external agent interop',
- shortValue: 'Official Python and TypeScript SDKs, plus MCP, code block, and A2A protocol',
+ 'No language-specific client SDKs: code calls the REST API directly (x-api-key header), with a published OpenAPI 3.1 specification for generating clients, and the `sim` CLI runs workflows and manages resources from a terminal or script. Extensibility is further extended by MCP (client + server), a sandboxed code-execution block (JS/Python), custom tools, and an Agent-to-Agent (A2A) protocol block for external agent interop',
+ shortValue: 'REST API with OpenAPI spec and CLI, plus MCP, code block, and A2A protocol',
confidence: 'verified',
sources: [
{
- url: 'https://docs.sim.ai/api-reference/python',
- label: 'Sim Docs: Python SDK',
- asOf: '2026-07-04',
+ url: 'https://docs.sim.ai/api-reference/getting-started',
+ label: 'Sim Docs: API Reference - Getting Started',
+ asOf: '2026-11-10',
},
{
- url: 'https://docs.sim.ai/api-reference/typescript',
- label: 'Sim Docs: TypeScript SDK',
- asOf: '2026-07-04',
+ url: 'https://docs.sim.ai/cli',
+ label: 'Sim Docs: CLI',
+ asOf: '2026-11-10',
},
{
url: 'https://docs.sim.ai/agents/mcp',
diff --git a/biome.json b/biome.json
index 9dc657a52b0..2da1565aa06 100644
--- a/biome.json
+++ b/biome.json
@@ -170,8 +170,7 @@
"packages/sim-cli/**",
"packages/sim-setup/**",
"packages/db/script-migrations/**",
- "packages/logger/src/**",
- "packages/ts-sdk/examples/**"
+ "packages/logger/src/**"
],
"linter": { "rules": { "suspicious": { "noConsole": "off" } } }
}
diff --git a/bun.lock b/bun.lock
index a8fff39dd7d..e1b0732c9ec 100644
--- a/bun.lock
+++ b/bun.lock
@@ -822,16 +822,6 @@
"stripe",
],
},
- "packages/ts-sdk": {
- "name": "simstudio-ts-sdk",
- "version": "0.2.1",
- "devDependencies": {
- "@sim/tsconfig": "workspace:*",
- "@types/node": "24.19.2",
- "typescript": "^7.0.2",
- "vitest": "^5.1.0",
- },
- },
"packages/tsconfig": {
"name": "@sim/tsconfig",
"version": "0.0.0",
@@ -4320,8 +4310,6 @@
"simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="],
- "simstudio-ts-sdk": ["simstudio-ts-sdk@workspace:packages/ts-sdk"],
-
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
"slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="],
diff --git a/knip.jsonc b/knip.jsonc
index 48d5bdd9482..723e66db7e0 100644
--- a/knip.jsonc
+++ b/knip.jsonc
@@ -7,7 +7,7 @@
// On by default: every workspace's `exports` map serves only this monorepo, so an entry export
// no workspace imports is dead. The workspaces below turn it off because their entries are
// public or standalone: the apps (framework entries such as Next routes and Trigger tasks), the
- // published packages (cli, sim-cli, sim-setup, ts-sdk), db (standalone scripts), and the root
+ // published packages (cli, sim-cli, sim-setup), db (standalone scripts), and the root
// (scripts). A new private package is checked without opting in.
"includeEntryExports": true,
"workspaces": {
@@ -168,7 +168,6 @@
"entry": ["src/index.ts", "src/runtime.ts", "scripts/*.ts"]
},
"packages/sim-setup": { "includeEntryExports": false },
- "packages/ts-sdk": { "includeEntryExports": false, "entry": ["examples/*.ts"] },
// This shared config is consumed by apps that own the Next dependency.
"packages/tsconfig": { "ignoreUnresolved": ["next"] }
}
diff --git a/packages/README.md b/packages/README.md
index 8c8856daf6c..85981be16a1 100644
--- a/packages/README.md
+++ b/packages/README.md
@@ -14,5 +14,3 @@
| Package | npm | Description |
|---------|-----|-------------|
| [cli](./cli) | `simstudio` | Run Sim locally via Docker |
-| [ts-sdk](./ts-sdk) | `simstudio-ts-sdk` | TypeScript SDK for workflow execution |
-| [python-sdk](./python-sdk) | `simstudio-sdk` | Python SDK for workflow execution |
diff --git a/packages/python-sdk/.gitignore b/packages/python-sdk/.gitignore
deleted file mode 100644
index ec1f692f4ea..00000000000
--- a/packages/python-sdk/.gitignore
+++ /dev/null
@@ -1,87 +0,0 @@
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-pip-wheel-metadata/
-share/python-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-MANIFEST
-
-# PyInstaller
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.nox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*.cover
-*.py,cover
-.hypothesis/
-.pytest_cache/
-
-# Virtual environments
-.venv
-env/
-venv/
-ENV/
-env.bak/
-venv.bak/
-test_env/
-
-# IDEs
-.vscode/
-.idea/
-*.swp
-*.swo
-
-# OS
-.DS_Store
-Thumbs.db
-
-# Jupyter Notebook
-.ipynb_checkpoints
-
-# pyenv
-.python-version
-
-# Environments
-.env
-.env.local
-
-# mypy
-.mypy_cache/
-.dmypy.json
-dmypy.json
-
-# uv
-uv.lock
\ No newline at end of file
diff --git a/packages/python-sdk/README.md b/packages/python-sdk/README.md
deleted file mode 100644
index 97119468785..00000000000
--- a/packages/python-sdk/README.md
+++ /dev/null
@@ -1,573 +0,0 @@
-# Sim Python SDK
-
-The official Python SDK for [Sim](https://sim.ai), allowing you to execute workflows programmatically from your Python applications.
-
-## Server compatibility
-
-`0.2.x` talks to the v2 API and has no fallback to the older endpoints, so it requires a Sim deployment that serves `POST /api/v2/workflows/{id}/execute`. That surface is newer than the endpoints `0.1.x` used. If it is unavailable, `execute_workflow` raises `SimStudioError('HTTP 404: Not Found')` — upgrade the server or pin `simstudio-sdk<0.2`, which keeps using `/api/workflows/{id}/execute` and `/api/jobs/{id}`.
-
-## Upgrading from 0.1.x to 0.2.0
-
-`0.2.0` is a breaking release.
-
-- **Requests move to `/api/v2`.** `execute_workflow` posts to `/api/v2/workflows/{workflow_id}/execute`, sends the workflow input nested under `input`, and carries `async` / `executionTimeoutSeconds` in the body instead of the `X-Execution-Mode` and `X-Execution-Timeout-Seconds` headers.
-- **`AsyncExecutionResult.job_id` is now `run_id`,** and `execution_id` has been removed from that dataclass. Replace `result.job_id` with `result.run_id`.
-- **`get_job_status(job_id)` is legacy.** It still calls `/api/jobs/{job_id}` and only resolves IDs from a `0.1.x` async execution. For runs started by `0.2.x`, use `get_workflow_run(workflow_id, run_id)`, which reads `/api/v2/workflows/{workflow_id}/runs/{run_id}`.
-- **`WorkflowExecutionResult.success` is derived from the run status** rather than read from the response body, and is `True` only for `completed` and `paused` runs — so a run cancelled while it was in flight now reports `success=False`, as it did before the v2 migration. The new `WorkflowExecutionResult.status` field carries the server's terminal status (`'completed'`, `'failed'`, `'paused'` or `'cancelled'`), which is how you tell a cancelled run from a failed one.
-- **`metadata` is now built by the SDK,** with the keys `duration`, `runId`, `startTime` and `endTime`. The v2 response carries no execution logs or trace spans, so `logs` and `trace_spans` are always `None`; the pre-v2 `metadata['executionId']` is now `metadata['runId']`.
-
-Note one deliberate difference from the TypeScript SDK: a failed synchronous run *throws* there, but here it returns normally with `error` set and `status='failed'`.
-
-## Installation
-
-```bash
-pip install simstudio-sdk
-```
-
-## Quick Start
-
-```python
-import os
-from simstudio import SimStudioClient
-
-# Initialize the client
-client = SimStudioClient(
- api_key=os.getenv("SIM_API_KEY", "your-api-key-here"),
- base_url="https://www.sim.ai" # optional, defaults to https://www.sim.ai
-)
-
-# Execute a workflow
-try:
- result = client.execute_workflow("workflow-id")
- print("Workflow executed successfully:", result)
-except Exception as error:
- print("Workflow execution failed:", error)
-```
-
-## API Reference
-
-### SimStudioClient
-
-#### Constructor
-
-```python
-SimStudioClient(api_key: str, base_url: str = "https://www.sim.ai")
-```
-
-- `api_key` (str): Your Sim API key
-- `base_url` (str, optional): Base URL for the Sim API (defaults to `https://www.sim.ai`)
-
-#### Methods
-
-##### execute_workflow(workflow_id, input=None, *, timeout=30.0, stream=None, selected_outputs=None, async_execution=None, execution_timeout_seconds=None)
-
-Execute a workflow with optional input data.
-
-```python
-# With dict input (sent as the v2 input object)
-result = client.execute_workflow("workflow-id", {"message": "Hello, world!"})
-
-# With primitive input (sent as { input: { input: value } })
-result = client.execute_workflow("workflow-id", "NVDA")
-
-# With options (keyword-only arguments)
-result = client.execute_workflow(
- "workflow-id",
- {"message": "Hello"},
- timeout=60.0,
- async_execution=True,
- execution_timeout_seconds=3600,
-)
-```
-
-**Parameters:**
-- `workflow_id` (str): The ID of the workflow to execute
-- `input` (any, optional): Input data to pass to the workflow. Dicts become the v2 `input` object; primitives and lists become `{ input: value }` inside it. File objects are automatically converted to base64.
-- `timeout` (float, keyword-only): Timeout in seconds (default: 30.0)
-- `stream` (bool, keyword-only): Enable streaming responses
-- `selected_outputs` (list, keyword-only): Block outputs to stream (e.g., `["agent1.content"]`)
-- `async_execution` (bool, keyword-only): Execute asynchronously and return a run ID
-- `execution_timeout_seconds` (int, keyword-only): Server-side async execution cap from 1 to 604800 seconds. Requires `async_execution=True` and cannot extend the account policy.
-
-**Returns:** `WorkflowExecutionResult` or `AsyncExecutionResult`
-
-##### get_workflow_status(workflow_id)
-
-Get the status of a workflow (deployment status, etc.).
-
-```python
-status = client.get_workflow_status("workflow-id")
-print("Is deployed:", status.is_deployed)
-```
-
-**Parameters:**
-- `workflow_id` (str): The ID of the workflow
-
-**Returns:** `WorkflowStatus`
-
-##### validate_workflow(workflow_id)
-
-Validate that a workflow is ready for execution.
-
-```python
-is_ready = client.validate_workflow("workflow-id")
-if is_ready:
- # Workflow is deployed and ready
- pass
-```
-
-**Parameters:**
-- `workflow_id` (str): The ID of the workflow
-
-**Returns:** `bool`
-
-##### execute_workflow_sync(workflow_id, input=None, *, timeout=30.0, stream=None, selected_outputs=None)
-
-Execute a workflow synchronously (ensures non-async mode).
-
-```python
-result = client.execute_workflow_sync("workflow-id", {"data": "some input"}, timeout=60.0)
-```
-
-**Parameters:**
-- `workflow_id` (str): The ID of the workflow to execute
-- `input` (any, optional): Input data to pass to the workflow
-- `timeout` (float, keyword-only): Timeout in seconds (default: 30.0)
-- `stream` (bool, keyword-only): Enable streaming responses
-- `selected_outputs` (list, keyword-only): Block outputs to stream (e.g., `["agent1.content"]`)
-
-**Returns:** `WorkflowExecutionResult`
-
-##### get_workflow_run(workflow_id, run_id, *, include_output=None, selected_outputs=None)
-
-Get the status and optional outputs of a workflow run. Use the run ID returned by async execution.
-
-```python
-status = client.get_workflow_run(
- "workflow-id",
- "run-id",
- include_output=True,
- selected_outputs=["agent.content"]
-)
-print("Run status:", status["status"])
-```
-
-**Parameters:**
-- `workflow_id` (str): The workflow ID
-- `run_id` (str): The run ID returned from async execution
-- `include_output` (bool, keyword-only): Include the final output for completed executions
-- `selected_outputs` (list, keyword-only): Block output selectors to include
-
-**Returns:** `dict`
-
-##### get_job_status(job_id)
-
-Get the status of a job created through the legacy async execution endpoint. New integrations should use `get_workflow_run()` with a run ID.
-
-```python
-status = client.get_job_status("legacy-job-id")
-```
-
-**Returns:** `dict`
-
-##### execute_with_retry(workflow_id, input=None, *, timeout=30.0, stream=None, selected_outputs=None, async_execution=None, max_retries=3, initial_delay=1.0, max_delay=30.0, backoff_multiplier=2.0)
-
-Execute a workflow with automatic retry on rate limit errors.
-
-```python
-result = client.execute_with_retry(
- "workflow-id",
- {"message": "Hello"},
- timeout=30.0,
- max_retries=3,
- initial_delay=1.0,
- max_delay=30.0,
- backoff_multiplier=2.0
-)
-```
-
-**Parameters:**
-- `workflow_id` (str): The ID of the workflow to execute
-- `input` (any, optional): Input data to pass to the workflow
-- `timeout` (float, keyword-only): Timeout in seconds (default: 30.0)
-- `stream` (bool, keyword-only): Enable streaming responses
-- `selected_outputs` (list, keyword-only): Block outputs to stream
-- `async_execution` (bool, keyword-only): Execute asynchronously
-- `max_retries` (int, keyword-only): Maximum retry attempts (default: 3)
-- `initial_delay` (float, keyword-only): Initial delay in seconds (default: 1.0)
-- `max_delay` (float, keyword-only): Maximum delay in seconds (default: 30.0)
-- `backoff_multiplier` (float, keyword-only): Backoff multiplier (default: 2.0)
-
-**Returns:** `WorkflowExecutionResult` or `AsyncExecutionResult`
-
-##### get_rate_limit_info()
-
-Get current rate limit information from the last API response.
-
-```python
-rate_info = client.get_rate_limit_info()
-if rate_info:
- print("Remaining requests:", rate_info.remaining)
-```
-
-**Returns:** `RateLimitInfo` or `None`
-
-##### get_usage_limits()
-
-Get current usage limits and quota information.
-
-```python
-limits = client.get_usage_limits()
-print("Current usage:", limits.usage)
-```
-
-**Returns:** `UsageLimits`
-
-##### set_api_key(api_key)
-
-Update the API key.
-
-```python
-client.set_api_key("new-api-key")
-```
-
-##### set_base_url(base_url)
-
-Update the base URL.
-
-```python
-client.set_base_url("https://my-custom-domain.com")
-```
-
-##### close()
-
-Close the underlying HTTP session.
-
-```python
-client.close()
-```
-
-## Data Classes
-
-### WorkflowExecutionResult
-
-```python
-@dataclass
-class WorkflowExecutionResult:
- success: bool
- output: Optional[Any] = None
- error: Optional[str] = None
- logs: Optional[list] = None
- metadata: Optional[Dict[str, Any]] = None
- trace_spans: Optional[list] = None
- total_duration: Optional[float] = None
- status: Optional[str] = None
-```
-
-`success` is `True` only for the `completed` and `paused` statuses. `status` carries the server's terminal status verbatim, so a cancelled run (`success=False`, `error=None`) is distinguishable from a failed one.
-
-### WorkflowStatus
-
-```python
-@dataclass
-class WorkflowStatus:
- is_deployed: bool
- deployed_at: Optional[str] = None
- needs_redeployment: bool = False
-```
-
-### SimStudioError
-
-```python
-class SimStudioError(Exception):
- def __init__(self, message: str, code: Optional[str] = None, status: Optional[int] = None):
- super().__init__(message)
- self.code = code
- self.status = status
-```
-
-### AsyncExecutionResult
-
-```python
-@dataclass
-class AsyncExecutionResult:
- success: bool
- run_id: str
- status_url: str
- message: str = ""
- async_execution: bool = True
-```
-
-### RateLimitInfo
-
-```python
-@dataclass
-class RateLimitInfo:
- limit: int
- remaining: int
- reset: int
- retry_after: Optional[int] = None
-```
-
-### UsageLimits
-
-```python
-@dataclass
-class UsageLimits:
- success: bool
- rate_limit: Dict[str, Any]
- usage: Dict[str, Any]
-```
-
-## Examples
-
-### Basic Workflow Execution
-
-```python
-import os
-from simstudio import SimStudioClient
-
-client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
-
-def run_workflow():
- try:
- # Check if workflow is ready
- is_ready = client.validate_workflow("my-workflow-id")
- if not is_ready:
- raise Exception("Workflow is not deployed or ready")
-
- # Execute the workflow
- result = client.execute_workflow(
- "my-workflow-id",
- {
- "message": "Process this data",
- "user_id": "12345"
- }
- )
-
- if result.success:
- print("Output:", result.output)
- print("Duration:", result.metadata.get("duration") if result.metadata else None)
- else:
- print("Workflow failed:", result.error)
-
- except Exception as error:
- print("Error:", error)
-
-run_workflow()
-```
-
-### Error Handling
-
-```python
-from simstudio import SimStudioClient, SimStudioError
-import os
-
-client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
-
-def execute_with_error_handling():
- try:
- result = client.execute_workflow("workflow-id")
- return result
- except SimStudioError as error:
- if error.code == "UNAUTHORIZED":
- print("Invalid API key")
- elif error.code == "TIMEOUT":
- print("Workflow execution timed out")
- elif error.code == "USAGE_LIMIT_EXCEEDED":
- print("Usage limit exceeded")
- elif error.code == "INVALID_JSON":
- print("Invalid JSON in request body")
- else:
- print(f"Workflow error: {error}")
- raise
- except Exception as error:
- print(f"Unexpected error: {error}")
- raise
-```
-
-### Context Manager Usage
-
-```python
-from simstudio import SimStudioClient
-import os
-
-# Using context manager to automatically close the session
-with SimStudioClient(api_key=os.getenv("SIM_API_KEY")) as client:
- result = client.execute_workflow("workflow-id")
- print("Result:", result)
-# Session is automatically closed here
-```
-
-### Environment Configuration
-
-```python
-import os
-from simstudio import SimStudioClient
-
-# Using environment variables
-client = SimStudioClient(
- api_key=os.getenv("SIM_API_KEY"),
- base_url=os.getenv("SIM_BASE_URL", "https://www.sim.ai")
-)
-```
-
-### File Upload
-
-File objects are automatically detected and converted to base64 format. Include them in your input under the field name matching your workflow's API trigger input format:
-
-The SDK converts file objects to this format:
-```python
-{
- 'type': 'file',
- 'data': 'data:mime/type;base64,base64data',
- 'name': 'filename',
- 'mime': 'mime/type'
-}
-```
-
-Alternatively, you can manually provide files using the URL format:
-```python
-{
- 'type': 'url',
- 'data': 'https://example.com/file.pdf',
- 'name': 'file.pdf',
- 'mime': 'application/pdf'
-}
-```
-
-```python
-from simstudio import SimStudioClient
-import os
-
-client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
-
-# Upload a single file - include it under the field name from your API trigger
-with open('document.pdf', 'rb') as f:
- result = client.execute_workflow(
- 'workflow-id',
- {
- 'documents': [f], # Must match your workflow's "files" field name
- 'instructions': 'Analyze this document'
- }
- )
-
-# Upload multiple files
-with open('doc1.pdf', 'rb') as f1, open('doc2.pdf', 'rb') as f2:
- result = client.execute_workflow(
- 'workflow-id',
- {
- 'attachments': [f1, f2], # Must match your workflow's "files" field name
- 'query': 'Compare these documents'
- }
- )
-```
-
-### Batch Workflow Execution
-
-```python
-from simstudio import SimStudioClient
-import os
-
-client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
-
-def execute_workflows_batch(workflow_data_pairs):
- """Execute multiple workflows with different input data."""
- results = []
-
- for workflow_id, workflow_input in workflow_data_pairs:
- try:
- # Validate workflow before execution
- if not client.validate_workflow(workflow_id):
- print(f"Skipping {workflow_id}: not deployed")
- continue
-
- result = client.execute_workflow(workflow_id, workflow_input)
- results.append({
- "workflow_id": workflow_id,
- "success": result.success,
- "output": result.output,
- "error": result.error
- })
-
- except Exception as error:
- results.append({
- "workflow_id": workflow_id,
- "success": False,
- "error": str(error)
- })
-
- return results
-
-# Example usage
-workflows = [
- ("workflow-1", {"type": "analysis", "data": "sample1"}),
- ("workflow-2", {"type": "processing", "data": "sample2"}),
-]
-
-results = execute_workflows_batch(workflows)
-for result in results:
- print(f"Workflow {result['workflow_id']}: {'Success' if result['success'] else 'Failed'}")
-```
-
-## Getting Your API Key
-
-1. Log in to your [Sim](https://sim.ai) account
-2. Navigate to your workflow
-3. Click on "Deploy" to deploy your workflow
-4. Select or create an API key during the deployment process
-5. Copy the API key to use in your application
-
-## Development
-
-### Running Tests
-
-To run the tests locally:
-
-1. Clone the repository and navigate to the Python SDK directory:
- ```bash
- cd packages/python-sdk
- ```
-
-2. Create and activate a virtual environment:
- ```bash
- python3 -m venv venv
- source venv/bin/activate # On Windows: venv\Scripts\activate
- ```
-
-3. Install the package in development mode with test dependencies:
- ```bash
- pip install -e ".[dev]"
- ```
-
-4. Run the tests:
- ```bash
- pytest tests/ -v
- ```
-
-### Code Quality
-
-Run code quality checks:
-
-```bash
-# Code formatting
-black simstudio/
-
-# Linting
-flake8 simstudio/ --max-line-length=100
-
-# Type checking
-mypy simstudio/
-
-# Import sorting
-isort simstudio/
-```
-
-## Requirements
-
-- Python 3.8+
-- requests >= 2.25.0
-
-## License
-
-Apache-2.0
diff --git a/packages/python-sdk/examples/basic_usage.py b/packages/python-sdk/examples/basic_usage.py
deleted file mode 100644
index b4c93dec632..00000000000
--- a/packages/python-sdk/examples/basic_usage.py
+++ /dev/null
@@ -1,260 +0,0 @@
-#!/usr/bin/env python3
-"""
-Basic usage examples for the Sim Python SDK
-"""
-
-import os
-from simstudio import SimStudioClient, SimStudioError
-
-
-def basic_example():
- """Example 1: Basic workflow execution"""
- client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
-
- try:
- # Execute a workflow without input
- result = client.execute_workflow("your-workflow-id")
-
- if result.success:
- print("✅ Workflow executed successfully!")
- print(f"Output: {result.output}")
- if result.metadata:
- print(f"Duration: {result.metadata.get('duration')} ms")
- else:
- print(f"❌ Workflow failed: {result.error}")
-
- except SimStudioError as error:
- print(f"SDK Error: {error} (Code: {error.code})")
- except Exception as error:
- print(f"Unexpected error: {error}")
-
-
-def with_input_example():
- """Example 2: Workflow execution with input data"""
- client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
-
- try:
- result = client.execute_workflow(
- "your-workflow-id",
- input_data={
- "message": "Hello from Python SDK!",
- "user_id": "12345",
- "data": {
- "type": "analysis",
- "parameters": {
- "include_metadata": True,
- "format": "json"
- }
- }
- },
- timeout=60.0 # 60 seconds
- )
-
- if result.success:
- print("✅ Workflow executed successfully!")
- print(f"Output: {result.output}")
- if result.metadata:
- print(f"Duration: {result.metadata.get('duration')} ms")
- else:
- print(f"❌ Workflow failed: {result.error}")
-
- except SimStudioError as error:
- print(f"SDK Error: {error} (Code: {error.code})")
- except Exception as error:
- print(f"Unexpected error: {error}")
-
-
-def status_example():
- """Example 3: Workflow validation and status checking"""
- client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
-
- try:
- # Check if workflow is ready
- is_ready = client.validate_workflow("your-workflow-id")
- print(f"Workflow ready: {is_ready}")
-
- # Get detailed status
- status = client.get_workflow_status("your-workflow-id")
- print(f"Status: {{\n"
- f" deployed: {status.is_deployed},\n"
- f" needs_redeployment: {status.needs_redeployment},\n"
- f" deployed_at: {status.deployed_at}\n"
- f"}}")
-
- if status.is_deployed:
- # Execute the workflow
- result = client.execute_workflow("your-workflow-id")
- print(f"Result: {result}")
-
- except Exception as error:
- print(f"Error: {error}")
-
-
-def context_manager_example():
- """Example 4: Using context manager"""
- with SimStudioClient(api_key=os.getenv("SIM_API_KEY")) as client:
- try:
- result = client.execute_workflow("your-workflow-id")
- print(f"Result: {result}")
- except Exception as error:
- print(f"Error: {error}")
- # Session is automatically closed here
-
-
-def batch_execution_example():
- """Example 5: Batch workflow execution"""
- client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
-
- workflows = [
- ("workflow-1", {"type": "analysis", "data": "sample1"}),
- ("workflow-2", {"type": "processing", "data": "sample2"}),
- ("workflow-3", {"type": "validation", "data": "sample3"}),
- ]
-
- results = []
-
- for workflow_id, input_data in workflows:
- try:
- # Validate workflow before execution
- if not client.validate_workflow(workflow_id):
- print(f"⚠️ Skipping {workflow_id}: not deployed")
- continue
-
- result = client.execute_workflow(workflow_id, input_data)
- results.append({
- "workflow_id": workflow_id,
- "success": result.success,
- "output": result.output,
- "error": result.error
- })
-
- status = "✅ Success" if result.success else "❌ Failed"
- print(f"{status}: {workflow_id}")
-
- except SimStudioError as error:
- results.append({
- "workflow_id": workflow_id,
- "success": False,
- "error": str(error)
- })
- print(f"❌ SDK Error in {workflow_id}: {error}")
- except Exception as error:
- results.append({
- "workflow_id": workflow_id,
- "success": False,
- "error": str(error)
- })
- print(f"❌ Unexpected error in {workflow_id}: {error}")
-
- # Summary
- successful = sum(1 for r in results if r["success"])
- total = len(results)
- print(f"\n📊 Summary: {successful}/{total} workflows completed successfully")
-
- return results
-
-
-def streaming_example():
- """Example 6: Workflow execution with streaming"""
- client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
-
- try:
- result = client.execute_workflow(
- "your-workflow-id",
- input_data={"message": "Count to five"},
- stream=True,
- selected_outputs=["agent1.content"], # Use blockName.attribute format
- timeout=60.0
- )
-
- if result.success:
- print("✅ Workflow executed successfully!")
- print(f"Output: {result.output}")
- if result.metadata:
- print(f"Duration: {result.metadata.get('duration')} ms")
- else:
- print(f"❌ Workflow failed: {result.error}")
-
- except SimStudioError as error:
- print(f"SDK Error: {error} (Code: {error.code})")
- except Exception as error:
- print(f"Unexpected error: {error}")
-
-
-def error_handling_example():
- """Example 7: Comprehensive error handling"""
- client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
-
- try:
- result = client.execute_workflow("your-workflow-id")
-
- if result.success:
- print("✅ Workflow executed successfully!")
- print(f"Output: {result.output}")
- return result
- else:
- print(f"❌ Workflow failed: {result.error}")
- return result
- except SimStudioError as error:
- if error.code == "UNAUTHORIZED":
- print("❌ Invalid API key")
- elif error.code == "TIMEOUT":
- print("⏱️ Workflow execution timed out")
- elif error.code == "USAGE_LIMIT_EXCEEDED":
- print("💳 Usage limit exceeded")
- elif error.code == "INVALID_JSON":
- print("📝 Invalid JSON in request body")
- elif error.status == 404:
- print("🔍 Workflow not found")
- elif error.status == 403:
- print("🚫 Workflow is not deployed")
- else:
- print(f"⚠️ Workflow error: {error}")
- raise
- except Exception as error:
- print(f"💥 Unexpected error: {error}")
- raise
-
-
-if __name__ == "__main__":
- print("🚀 Running Sim Python SDK Examples\n")
-
- # Check if API key is set
- if not os.getenv("SIM_API_KEY"):
- print("❌ Please set SIM_API_KEY environment variable")
- exit(1)
-
- try:
- print("1️⃣ Basic Example:")
- basic_example()
- print("\n✅ Basic example completed\n")
-
- print("2️⃣ Input Example:")
- with_input_example()
- print("\n✅ Input example completed\n")
-
- print("3️⃣ Status Example:")
- status_example()
- print("\n✅ Status example completed\n")
-
- print("4️⃣ Context Manager Example:")
- context_manager_example()
- print("\n✅ Context manager example completed\n")
-
- print("5️⃣ Batch Execution Example:")
- batch_execution_example()
- print("\n✅ Batch execution example completed\n")
-
- print("6️⃣ Streaming Example:")
- streaming_example()
- print("\n✅ Streaming example completed\n")
-
- print("7️⃣ Error Handling Example:")
- error_handling_example()
- print("\n✅ Error handling example completed\n")
-
- except Exception as e:
- print(f"\n💥 Example failed: {e}")
- exit(1)
-
- print("🎉 All examples completed successfully!")
\ No newline at end of file
diff --git a/packages/python-sdk/examples/file_upload.py b/packages/python-sdk/examples/file_upload.py
deleted file mode 100644
index fc54d28cf2b..00000000000
--- a/packages/python-sdk/examples/file_upload.py
+++ /dev/null
@@ -1,55 +0,0 @@
-"""
-Example: Upload files with workflow execution
-
-This example demonstrates how to upload files when executing a workflow.
-Files are automatically detected and converted to base64 format.
-"""
-
-from simstudio import SimStudioClient
-import os
-
-
-def main():
- # Initialize the client
- api_key = os.getenv('SIM_API_KEY')
- if not api_key:
- raise ValueError('SIM_API_KEY environment variable is required')
-
- client = SimStudioClient(api_key=api_key)
-
- # Example 1: Upload a single file
- # Include file under the field name from your workflow's API trigger input format
- print("Example 1: Upload a single file")
- with open('document.pdf', 'rb') as f:
- result = client.execute_workflow(
- workflow_id='your-workflow-id',
- input_data={
- 'documents': [f], # Field name must match your API trigger's file input field
- 'instructions': 'Analyze this document'
- }
- )
-
- if result.success:
- print(f"Success! Output: {result.output}")
- else:
- print(f"Failed: {result.error}")
-
- # Example 2: Upload multiple files
- print("\nExample 2: Upload multiple files")
- with open('document1.pdf', 'rb') as f1, open('document2.pdf', 'rb') as f2:
- result = client.execute_workflow(
- workflow_id='your-workflow-id',
- input_data={
- 'attachments': [f1, f2], # Field name must match your API trigger's file input field
- 'query': 'Compare these documents'
- }
- )
-
- if result.success:
- print(f"Success! Output: {result.output}")
- else:
- print(f"Failed: {result.error}")
-
-
-if __name__ == '__main__':
- main()
diff --git a/packages/python-sdk/pyproject.toml b/packages/python-sdk/pyproject.toml
deleted file mode 100644
index a3a5cfbb2f1..00000000000
--- a/packages/python-sdk/pyproject.toml
+++ /dev/null
@@ -1,84 +0,0 @@
-[build-system]
-requires = ["setuptools>=61.0", "wheel"]
-build-backend = "setuptools.build_meta"
-
-[project]
-name = "simstudio-sdk"
-version = "0.2.1"
-authors = [
- {name = "Sim", email = "help@sim.ai"},
-]
-description = "Sim SDK - Execute workflows programmatically"
-readme = "README.md"
-license = {text = "Apache-2.0"}
-requires-python = ">=3.8"
-classifiers = [
- "Development Status :: 4 - Beta",
- "Intended Audience :: Developers",
- "License :: OSI Approved :: Apache Software License",
- "Operating System :: OS Independent",
- "Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.11",
- "Programming Language :: Python :: 3.12",
- "Topic :: Software Development :: Libraries :: Python Modules",
- "Topic :: Internet :: WWW/HTTP :: HTTP Servers",
- "Topic :: Scientific/Engineering :: Artificial Intelligence",
-]
-keywords = ["simstudio", "ai", "workflow", "sdk", "api", "automation"]
-dependencies = [
- "requests>=2.25.0",
- "typing-extensions>=4.0.0; python_version<'3.10'",
-]
-
-[project.optional-dependencies]
-dev = [
- "pytest>=6.0.0",
- "pytest-asyncio>=0.18.0",
- "black>=22.0.0",
- "flake8>=4.0.0",
- "mypy>=0.910",
- "isort>=5.0.0",
- "types-requests>=2.25.0",
-]
-
-[project.urls]
-Homepage = "https://sim.ai"
-Documentation = "https://docs.sim.ai"
-Repository = "https://github.com/simstudioai/sim"
-"Bug Reports" = "https://github.com/simstudioai/sim/issues"
-
-[tool.setuptools.packages.find]
-where = ["."]
-include = ["simstudio*"]
-
-[tool.black]
-line-length = 100
-target-version = ['py38', 'py39', 'py310', 'py311', 'py312']
-
-[tool.isort]
-profile = "black"
-line_length = 100
-
-[tool.mypy]
-python_version = "3.8"
-warn_return_any = true
-warn_unused_configs = true
-disallow_untyped_defs = true
-disallow_incomplete_defs = true
-check_untyped_defs = true
-disallow_untyped_decorators = true
-no_implicit_optional = true
-warn_redundant_casts = true
-warn_unused_ignores = true
-warn_no_return = true
-warn_unreachable = true
-strict_equality = true
-
-[tool.pytest.ini_options]
-testpaths = ["tests"]
-python_files = ["test_*.py"]
-python_classes = ["Test*"]
-python_functions = ["test_*"]
\ No newline at end of file
diff --git a/packages/python-sdk/setup.py b/packages/python-sdk/setup.py
deleted file mode 100644
index c1d136a676f..00000000000
--- a/packages/python-sdk/setup.py
+++ /dev/null
@@ -1,51 +0,0 @@
-from setuptools import setup, find_packages
-
-with open("README.md", "r", encoding="utf-8") as fh:
- long_description = fh.read()
-
-setup(
- name="simstudio-sdk",
- version="0.1.1",
- author="Sim",
- author_email="help@sim.ai",
- description="Sim SDK - Execute workflows programmatically",
- long_description=long_description,
- long_description_content_type="text/markdown",
- url="https://github.com/simstudioai/sim",
- packages=find_packages(),
- classifiers=[
- "Development Status :: 4 - Beta",
- "Intended Audience :: Developers",
- "License :: OSI Approved :: Apache Software License",
- "Operating System :: OS Independent",
- "Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.11",
- "Programming Language :: Python :: 3.12",
- ],
- python_requires=">=3.8",
- install_requires=[
- "requests>=2.25.0",
- "typing-extensions>=4.0.0; python_version<'3.10'",
- ],
- extras_require={
- "dev": [
- "pytest>=6.0.0",
- "pytest-asyncio>=0.18.0",
- "black>=22.0.0",
- "flake8>=4.0.0",
- "mypy>=0.910",
- ],
- "test": [
- "pytest>=6.0.0",
- ],
- },
- keywords=["simstudio", "ai", "workflow", "sdk", "api", "automation"],
- project_urls={
- "Bug Reports": "https://github.com/simstudioai/sim/issues",
- "Source": "https://github.com/simstudioai/sim",
- "Documentation": "https://docs.sim.ai",
- },
-)
\ No newline at end of file
diff --git a/packages/python-sdk/simstudio/__init__.py b/packages/python-sdk/simstudio/__init__.py
deleted file mode 100644
index 4be050fbc48..00000000000
--- a/packages/python-sdk/simstudio/__init__.py
+++ /dev/null
@@ -1,708 +0,0 @@
-"""
-Sim SDK for Python
-
-Official Python SDK for Sim, allowing you to execute workflows programmatically.
-"""
-
-from typing import Any, Dict, Optional, Union
-from dataclasses import dataclass
-from datetime import datetime
-import time
-import random
-import os
-import platform
-
-import requests
-
-MAX_EXECUTION_TIMEOUT_SECONDS = 604_800
-
-# Run statuses that count as a successful synchronous execution. Deliberately a
-# whitelist: a status later added to the API then defaults to "not successful"
-# rather than silently reporting True.
-_SUCCESSFUL_RUN_STATUSES = ('completed', 'paused')
-
-__version__ = "0.2.1"
-
-
-def _client_headers() -> Dict[str, str]:
- """
- Identify this SDK to the API on every request, the way every official Sim
- client does, so a server log line or analytics event can say which client
- made the call. ``X-Sim-Client-Info`` is the header the server reads.
- """
- python_version = platform.python_version()
- return {
- 'User-Agent': f'simstudio-python-sdk/{__version__} python/{python_version}',
- 'X-Sim-Client-Info': f'sdk-python/{__version__}; python/{python_version}',
- }
-
-__all__ = [
- "SimStudioClient",
- "SimStudioError",
- "WorkflowExecutionResult",
- "WorkflowStatus",
- "AsyncExecutionResult",
- "RateLimitInfo",
- "UsageLimits",
-]
-
-
-@dataclass
-class WorkflowExecutionResult:
- """
- Result of a workflow execution.
-
- ``success`` is True only for the 'completed' and 'paused' statuses, so a
- run the server cancels and a run that fails both report False. Read
- ``status`` to tell those apart -- it carries the server's own terminal
- status ('completed', 'failed', 'paused' or 'cancelled').
- """
- success: bool
- output: Optional[Any] = None
- error: Optional[str] = None
- logs: Optional[list] = None
- metadata: Optional[Dict[str, Any]] = None
- trace_spans: Optional[list] = None
- total_duration: Optional[float] = None
- status: Optional[str] = None
-
-
-@dataclass
-class WorkflowStatus:
- """Status of a workflow."""
- is_deployed: bool
- deployed_at: Optional[str] = None
- needs_redeployment: bool = False
-
-
-@dataclass
-class AsyncExecutionResult:
- """Result of an async workflow execution."""
- success: bool
- run_id: str
- status_url: str
- message: str = ""
- async_execution: bool = True
-
-
-@dataclass
-class RateLimitInfo:
- """
- Rate limit information from API response headers.
-
- ``reset`` is epoch milliseconds when the server sends the ISO 8601
- ``X-RateLimit-Reset`` the v2 API uses, and epoch seconds for the bare
- integer older endpoints sent. ``retry_after`` is milliseconds.
- """
- limit: int
- remaining: int
- reset: int
- retry_after: Optional[int] = None
-
-
-@dataclass
-class UsageLimits:
- """Usage limits and quota information."""
- success: bool
- rate_limit: Dict[str, Any]
- usage: Dict[str, Any]
-
-
-class SimStudioError(Exception):
- """Exception raised for Sim API errors."""
-
- def __init__(self, message: str, code: Optional[str] = None, status: Optional[int] = None):
- super().__init__(message)
- self.code = code
- self.status = status
-
-
-def _parse_reset_header(value: str) -> int:
- """
- Parse the ``X-RateLimit-Reset`` header, matching the TypeScript SDK.
-
- The v2 API sends an ISO 8601 timestamp, which yields epoch milliseconds;
- older endpoints sent an epoch integer, which is kept as-is. A quota hint
- must never take down the call it rode in on, so an unrecognised value
- degrades to 0 rather than raising.
- """
- if value.isdecimal():
- return int(value)
- try:
- return int(datetime.fromisoformat(value.replace('Z', '+00:00')).timestamp() * 1000)
- except ValueError:
- return 0
-
-
-class SimStudioClient:
- """
- Sim API client for executing workflows programmatically.
-
- Args:
- api_key: Your Sim API key
- base_url: Base URL for the Sim API (defaults to https://www.sim.ai)
- """
-
- def __init__(self, api_key: str, base_url: str = "https://www.sim.ai"):
- self.api_key = api_key
- self.base_url = base_url.rstrip('/')
- self._session = requests.Session()
- self._session.headers.update({
- **_client_headers(),
- 'X-API-Key': self.api_key,
- 'Content-Type': 'application/json',
- })
- self._rate_limit_info: Optional[RateLimitInfo] = None
-
- def _convert_files_to_base64(self, value: Any) -> Any:
- """
- Convert file objects in input to API format (base64).
- Recursively processes nested dicts and lists.
- """
- import base64
-
- # Check if this is a file-like object
- if hasattr(value, 'read') and callable(value.read):
- # Save current position if seekable
- initial_pos = value.tell() if hasattr(value, 'tell') else None
-
- # Read file bytes
- file_bytes = value.read()
-
- # Restore position if seekable
- if initial_pos is not None and hasattr(value, 'seek'):
- value.seek(initial_pos)
-
- # Encode to base64
- base64_data = base64.b64encode(file_bytes).decode('utf-8')
-
- # Get file metadata
- filename = getattr(value, 'name', 'file')
- if isinstance(filename, str):
- filename = os.path.basename(filename)
-
- content_type = getattr(value, 'content_type', 'application/octet-stream')
-
- return {
- 'type': 'file',
- 'data': f'data:{content_type};base64,{base64_data}',
- 'name': filename,
- 'mime': content_type
- }
-
- # Recursively process lists
- if isinstance(value, list):
- return [self._convert_files_to_base64(item) for item in value]
-
- # Recursively process dicts
- if isinstance(value, dict):
- return {k: self._convert_files_to_base64(v) for k, v in value.items()}
-
- return value
-
- def execute_workflow(
- self,
- workflow_id: str,
- input: Optional[Any] = None,
- *,
- timeout: float = 30.0,
- stream: Optional[bool] = None,
- selected_outputs: Optional[list] = None,
- async_execution: Optional[bool] = None,
- execution_timeout_seconds: Optional[int] = None
- ) -> Union[WorkflowExecutionResult, AsyncExecutionResult]:
- """
- Execute a workflow with optional input data.
- If async_execution is True, returns immediately with a run ID.
-
- File objects in input will be automatically detected and converted to base64.
-
- Args:
- workflow_id: The ID of the workflow to execute
- input: Input data to pass to the workflow, sent nested under the request
- body's 'input' field. A dict becomes the workflow input as-is; any
- other value (string, number, bool, list) is wrapped as
- {'input': value}. File-like objects within it are automatically
- converted to base64.
- timeout: Timeout in seconds (default: 30.0)
- stream: Enable streaming responses (default: None)
- selected_outputs: Block outputs to stream (e.g., ["agent1.content"])
- async_execution: Execute asynchronously (default: None)
- execution_timeout_seconds: Server-side async execution cap in seconds (1-604800)
-
- Returns:
- WorkflowExecutionResult or AsyncExecutionResult object
-
- Raises:
- SimStudioError: If the workflow execution fails
- """
- url = f"{self.base_url}/api/v2/workflows/{workflow_id}/execute"
-
- if execution_timeout_seconds is not None:
- if not async_execution:
- raise SimStudioError(
- 'execution_timeout_seconds is supported only for async executions',
- 'INVALID_EXECUTION_TIMEOUT'
- )
- if (
- isinstance(execution_timeout_seconds, bool)
- or not isinstance(execution_timeout_seconds, int)
- or execution_timeout_seconds < 1
- or execution_timeout_seconds > MAX_EXECUTION_TIMEOUT_SECONDS
- ):
- raise SimStudioError(
- f'execution_timeout_seconds must be an integer between 1 and {MAX_EXECUTION_TIMEOUT_SECONDS}',
- 'INVALID_EXECUTION_TIMEOUT'
- )
-
- headers = self._session.headers.copy()
-
- try:
- workflow_input = {}
- if input is not None:
- if isinstance(input, dict):
- workflow_input = input.copy()
- else:
- workflow_input = {'input': input}
-
- workflow_input = self._convert_files_to_base64(workflow_input)
- body = {'input': workflow_input}
-
- if stream is not None:
- body['stream'] = stream
- if selected_outputs is not None:
- body['selectedOutputs'] = selected_outputs
- if async_execution is not None:
- body['async'] = async_execution
- if execution_timeout_seconds is not None:
- body['executionTimeoutSeconds'] = execution_timeout_seconds
-
- response = self._session.post(
- url,
- json=body,
- headers=headers,
- timeout=timeout
- )
-
- # Update rate limit info
- self._update_rate_limit_info(response)
-
- # Handle rate limiting
- if response.status_code == 429:
- retry_after = self._rate_limit_info.retry_after if self._rate_limit_info else 1000
- raise SimStudioError(
- f'Rate limit exceeded. Retry after {retry_after}ms',
- 'RATE_LIMIT_EXCEEDED',
- 429
- )
-
- if not response.ok:
- try:
- error_data = response.json()
- error = error_data.get('error', {})
- error_message = error.get('message', f'HTTP {response.status_code}: {response.reason}')
- error_code = error.get('code')
- except (ValueError, KeyError):
- error_message = f'HTTP {response.status_code}: {response.reason}'
- error_code = None
-
- raise SimStudioError(error_message, error_code, response.status_code)
-
- result = response.json()
- if 'data' not in result:
- raise SimStudioError('Invalid v2 workflow execution response', 'EXECUTION_ERROR')
- result_data = result['data']
-
- if response.status_code == 202:
- if 'runId' not in result_data or 'statusUrl' not in result_data:
- raise SimStudioError('Invalid v2 async execution response', 'EXECUTION_ERROR')
- return AsyncExecutionResult(
- success=True,
- run_id=result_data['runId'],
- status_url=result_data['statusUrl'],
- message='Workflow execution queued',
- async_execution=True
- )
-
- execution_error = result_data.get('error')
- status = result_data.get('status')
- return WorkflowExecutionResult(
- success=status in _SUCCESSFUL_RUN_STATUSES,
- output=result_data.get('output'),
- error=execution_error.get('message') if execution_error else None,
- metadata={
- 'duration': result_data.get('durationMs'),
- 'runId': result_data['runId'],
- 'startTime': result_data.get('startedAt'),
- 'endTime': result_data.get('endedAt')
- },
- total_duration=result_data.get('durationMs'),
- status=status
- )
-
- except requests.Timeout:
- raise SimStudioError(f'Workflow execution timed out after {timeout} seconds', 'TIMEOUT')
- except requests.RequestException as e:
- raise SimStudioError(f'Failed to execute workflow: {str(e)}', 'EXECUTION_ERROR')
-
- def get_workflow_status(self, workflow_id: str) -> WorkflowStatus:
- """
- Get the status of a workflow (deployment status, etc.).
-
- Args:
- workflow_id: The ID of the workflow
-
- Returns:
- WorkflowStatus object containing the workflow status
-
- Raises:
- SimStudioError: If getting the status fails
- """
- url = f"{self.base_url}/api/workflows/{workflow_id}/status"
-
- try:
- response = self._session.get(url)
-
- if not response.ok:
- try:
- error_data = response.json()
- error_message = error_data.get('error', f'HTTP {response.status_code}: {response.reason}')
- error_code = error_data.get('code')
- except (ValueError, KeyError):
- error_message = f'HTTP {response.status_code}: {response.reason}'
- error_code = None
-
- raise SimStudioError(error_message, error_code, response.status_code)
-
- status_data = response.json()
-
- return WorkflowStatus(
- is_deployed=status_data.get('isDeployed', False),
- deployed_at=status_data.get('deployedAt'),
- needs_redeployment=status_data.get('needsRedeployment', False)
- )
-
- except requests.RequestException as e:
- raise SimStudioError(f'Failed to get workflow status: {str(e)}', 'STATUS_ERROR')
-
- def validate_workflow(self, workflow_id: str) -> bool:
- """
- Validate that a workflow is ready for execution.
-
- Args:
- workflow_id: The ID of the workflow
-
- Returns:
- True if the workflow is deployed and ready, False otherwise
- """
- try:
- status = self.get_workflow_status(workflow_id)
- return status.is_deployed
- except SimStudioError:
- return False
-
- def execute_workflow_sync(
- self,
- workflow_id: str,
- input: Optional[Any] = None,
- *,
- timeout: float = 30.0,
- stream: Optional[bool] = None,
- selected_outputs: Optional[list] = None
- ) -> WorkflowExecutionResult:
- """
- Execute a workflow synchronously (ensures non-async mode).
-
- Args:
- workflow_id: The ID of the workflow to execute
- input: Input data to pass to the workflow (can include file-like objects)
- timeout: Timeout for the initial request in seconds
- stream: Enable streaming responses (default: None)
- selected_outputs: Block outputs to stream (e.g., ["agent1.content"])
-
- Returns:
- WorkflowExecutionResult object containing the execution result
-
- Raises:
- SimStudioError: If the workflow execution fails
- """
- return self.execute_workflow(
- workflow_id,
- input,
- timeout=timeout,
- stream=stream,
- selected_outputs=selected_outputs,
- async_execution=False
- )
-
- def set_api_key(self, api_key: str) -> None:
- """
- Update the API key.
-
- Args:
- api_key: New API key
- """
- self.api_key = api_key
- self._session.headers.update({'X-API-Key': api_key})
-
- def set_base_url(self, base_url: str) -> None:
- """
- Update the base URL.
-
- Args:
- base_url: New base URL
- """
- self.base_url = base_url.rstrip('/')
-
- def close(self) -> None:
- """Close the underlying HTTP session."""
- self._session.close()
-
- def get_job_status(self, job_id: str) -> Dict[str, Any]:
- """
- Get the status of a legacy async job.
-
- Args:
- job_id: The job ID returned from legacy async execution
-
- Returns:
- Dictionary containing the job status
-
- Raises:
- SimStudioError: If getting the status fails
- """
- url = f"{self.base_url}/api/jobs/{job_id}"
-
- try:
- response = self._session.get(url)
-
- self._update_rate_limit_info(response)
-
- if not response.ok:
- try:
- error_data = response.json()
- error_message = error_data.get('error', f'HTTP {response.status_code}: {response.reason}')
- error_code = error_data.get('code')
- except (ValueError, KeyError):
- error_message = f'HTTP {response.status_code}: {response.reason}'
- error_code = None
-
- raise SimStudioError(error_message, error_code, response.status_code)
-
- return response.json()
-
- except requests.RequestException as e:
- raise SimStudioError(f'Failed to get job status: {str(e)}', 'STATUS_ERROR')
-
- def get_workflow_run(
- self,
- workflow_id: str,
- run_id: str,
- *,
- include_output: Optional[bool] = None,
- selected_outputs: Optional[list] = None
- ) -> Dict[str, Any]:
- """
- Get a workflow run's current status and optional outputs from the v2 API.
-
- Args:
- workflow_id: The workflow ID
- run_id: The run ID returned from async execution
- include_output: Include the final output for completed executions
- selected_outputs: Block output selectors to include
-
- Returns:
- Dictionary containing the run status
-
- Raises:
- SimStudioError: If getting the status fails
- """
- url = f"{self.base_url}/api/v2/workflows/{workflow_id}/runs/{run_id}"
- params = {}
- if include_output is not None:
- params['includeOutput'] = str(include_output).lower()
- if selected_outputs:
- params['selectedOutputs'] = ','.join(selected_outputs)
-
- try:
- response = self._session.get(url, params=params or None)
-
- self._update_rate_limit_info(response)
-
- if not response.ok:
- try:
- error_data = response.json()
- error = error_data.get('error', {})
- error_message = error.get('message', f'HTTP {response.status_code}: {response.reason}')
- error_code = error.get('code')
- except (ValueError, KeyError):
- error_message = f'HTTP {response.status_code}: {response.reason}'
- error_code = None
-
- raise SimStudioError(error_message, error_code, response.status_code)
-
- result = response.json()
- if 'data' not in result:
- raise SimStudioError('Invalid v2 workflow run response', 'STATUS_ERROR')
- return result['data']
-
- except requests.RequestException as e:
- raise SimStudioError(f'Failed to get workflow run: {str(e)}', 'STATUS_ERROR')
-
- def execute_with_retry(
- self,
- workflow_id: str,
- input: Optional[Any] = None,
- *,
- timeout: float = 30.0,
- stream: Optional[bool] = None,
- selected_outputs: Optional[list] = None,
- async_execution: Optional[bool] = None,
- execution_timeout_seconds: Optional[int] = None,
- max_retries: int = 3,
- initial_delay: float = 1.0,
- max_delay: float = 30.0,
- backoff_multiplier: float = 2.0
- ) -> Union[WorkflowExecutionResult, AsyncExecutionResult]:
- """
- Execute workflow with automatic retry on rate limit.
-
- Args:
- workflow_id: The ID of the workflow to execute
- input: Input data to pass to the workflow (can include file-like objects)
- timeout: Timeout in seconds
- stream: Enable streaming responses
- selected_outputs: Block outputs to stream
- async_execution: Execute asynchronously
- execution_timeout_seconds: Server-side async execution cap in seconds (1-604800)
- max_retries: Maximum number of retries (default: 3)
- initial_delay: Initial delay in seconds (default: 1.0)
- max_delay: Maximum delay in seconds (default: 30.0)
- backoff_multiplier: Backoff multiplier (default: 2.0)
-
- Returns:
- WorkflowExecutionResult or AsyncExecutionResult object
-
- Raises:
- SimStudioError: If max retries exceeded or other error occurs
- """
- last_error = None
- delay = initial_delay
-
- for attempt in range(max_retries + 1):
- try:
- return self.execute_workflow(
- workflow_id,
- input,
- timeout=timeout,
- stream=stream,
- selected_outputs=selected_outputs,
- async_execution=async_execution,
- execution_timeout_seconds=execution_timeout_seconds,
- )
- except SimStudioError as e:
- if e.code != 'RATE_LIMIT_EXCEEDED':
- raise
-
- last_error = e
-
- # Don't retry after last attempt
- if attempt == max_retries:
- break
-
- # Use retry-after if provided, otherwise use exponential backoff
- wait_time = (
- self._rate_limit_info.retry_after / 1000
- if self._rate_limit_info and self._rate_limit_info.retry_after
- else min(delay, max_delay)
- )
-
- # Add jitter (±25%)
- jitter = wait_time * (0.75 + random.random() * 0.5)
-
- time.sleep(jitter)
-
- # Exponential backoff for next attempt
- delay *= backoff_multiplier
-
- raise last_error or SimStudioError('Max retries exceeded', 'MAX_RETRIES_EXCEEDED')
-
- def get_rate_limit_info(self) -> Optional[RateLimitInfo]:
- """
- Get current rate limit information.
-
- Returns:
- RateLimitInfo object or None if no rate limit info available
- """
- return self._rate_limit_info
-
- def _update_rate_limit_info(self, response: requests.Response) -> None:
- """
- Update rate limit info from response headers.
-
- Args:
- response: The response object to extract headers from
- """
- limit = response.headers.get('x-ratelimit-limit')
- remaining = response.headers.get('x-ratelimit-remaining')
- reset = response.headers.get('x-ratelimit-reset')
- retry_after = response.headers.get('retry-after')
-
- if limit or remaining or reset:
- self._rate_limit_info = RateLimitInfo(
- limit=int(limit) if limit else 0,
- remaining=int(remaining) if remaining else 0,
- reset=_parse_reset_header(reset) if reset else 0,
- retry_after=int(retry_after) * 1000 if retry_after else None
- )
-
- def get_usage_limits(self) -> UsageLimits:
- """
- Get current usage limits and quota information.
-
- Returns:
- UsageLimits object containing usage and quota data
-
- Raises:
- SimStudioError: If getting usage limits fails
- """
- url = f"{self.base_url}/api/users/me/usage-limits"
-
- try:
- response = self._session.get(url)
-
- self._update_rate_limit_info(response)
-
- if not response.ok:
- try:
- error_data = response.json()
- error_message = error_data.get('error', f'HTTP {response.status_code}: {response.reason}')
- error_code = error_data.get('code')
- except (ValueError, KeyError):
- error_message = f'HTTP {response.status_code}: {response.reason}'
- error_code = None
-
- raise SimStudioError(error_message, error_code, response.status_code)
-
- data = response.json()
-
- return UsageLimits(
- success=data.get('success', True),
- rate_limit=data.get('rateLimit', {}),
- usage=data.get('usage', {})
- )
-
- except requests.RequestException as e:
- raise SimStudioError(f'Failed to get usage limits: {str(e)}', 'USAGE_ERROR')
-
- def __enter__(self):
- """Context manager entry."""
- return self
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- """Context manager exit."""
- self.close()
-
-
-# For backward compatibility
-Client = SimStudioClient
diff --git a/packages/python-sdk/tests/__init__.py b/packages/python-sdk/tests/__init__.py
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/packages/python-sdk/tests/test_client.py b/packages/python-sdk/tests/test_client.py
deleted file mode 100644
index de6e7029991..00000000000
--- a/packages/python-sdk/tests/test_client.py
+++ /dev/null
@@ -1,778 +0,0 @@
-"""
-Tests for the Sim Python SDK
-"""
-
-import platform
-
-import pytest
-from unittest.mock import Mock, patch
-from simstudio import SimStudioClient, SimStudioError, WorkflowExecutionResult, WorkflowStatus, __version__
-
-
-def v2_execution_response(output=None, status="completed", error=None):
- return {
- "data": {
- "runId": "execution-123",
- "workflowId": "workflow-id",
- "status": status,
- "output": {} if output is None else output,
- "error": error,
- "startedAt": "2026-08-11T12:00:00.000Z",
- "endedAt": "2026-08-11T12:00:00.010Z",
- "durationMs": 10
- }
- }
-
-
-def mock_execution_post(mock_post, status="completed", error=None, headers=None):
- """Wire a mocked 200 v2 execution response with the given terminal status."""
- mock_response = Mock()
- mock_response.ok = True
- mock_response.status_code = 200
- mock_response.json.return_value = v2_execution_response(status=status, error=error)
- mock_response.headers.get.side_effect = lambda h: (headers or {}).get(h)
- mock_post.return_value = mock_response
- return mock_response
-
-
-def test_simstudio_client_initialization():
- """Test SimStudioClient initialization."""
- client = SimStudioClient(api_key="test-api-key", base_url="https://test.sim.ai")
- assert client.api_key == "test-api-key"
- assert client.base_url == "https://test.sim.ai"
-
-
-def test_simstudio_client_default_base_url():
- """Test SimStudioClient with default base URL."""
- client = SimStudioClient(api_key="test-api-key")
- assert client.api_key == "test-api-key"
- assert client.base_url == "https://www.sim.ai"
-
-
-def test_set_api_key():
- """Test setting a new API key."""
- client = SimStudioClient(api_key="test-api-key")
- client.set_api_key("new-api-key")
- assert client.api_key == "new-api-key"
-
-
-def test_set_base_url():
- """Test setting a new base URL."""
- client = SimStudioClient(api_key="test-api-key")
- client.set_base_url("https://new.sim.ai/")
- assert client.base_url == "https://new.sim.ai"
-
-
-def test_set_base_url_strips_trailing_slash():
- """Test that base URL strips trailing slash."""
- client = SimStudioClient(api_key="test-api-key")
- client.set_base_url("https://test.sim.ai/")
- assert client.base_url == "https://test.sim.ai"
-
-
-@patch('simstudio.requests.Session.get')
-def test_validate_workflow_returns_false_on_error(mock_get):
- """Test that validate_workflow returns False when request fails."""
- mock_get.side_effect = SimStudioError("Network error")
-
- client = SimStudioClient(api_key="test-api-key")
- result = client.validate_workflow("test-workflow-id")
-
- assert result is False
- mock_get.assert_called_once_with("https://www.sim.ai/api/workflows/test-workflow-id/status")
-
-
-def test_simstudio_error():
- """Test SimStudioError creation."""
- error = SimStudioError("Test error", "TEST_CODE", 400)
- assert str(error) == "Test error"
- assert error.code == "TEST_CODE"
- assert error.status == 400
-
-
-def test_workflow_execution_result():
- """Test WorkflowExecutionResult data class."""
- result = WorkflowExecutionResult(
- success=True,
- output={"data": "test"},
- metadata={"duration": 1000}
- )
- assert result.success is True
- assert result.output == {"data": "test"}
- assert result.metadata == {"duration": 1000}
-
-
-def test_workflow_status():
- """Test WorkflowStatus data class."""
- status = WorkflowStatus(
- is_deployed=True,
- deployed_at="2023-01-01T00:00:00Z",
- needs_redeployment=False
- )
- assert status.is_deployed is True
- assert status.deployed_at == "2023-01-01T00:00:00Z"
- assert status.needs_redeployment is False
-
-
-@patch('simstudio.requests.Session.close')
-def test_context_manager(mock_close):
- """Test SimStudioClient as context manager."""
- with SimStudioClient(api_key="test-api-key") as client:
- assert client.api_key == "test-api-key"
- mock_close.assert_called_once()
-
-
-@patch('simstudio.requests.Session.post')
-def test_async_execution_returns_run_id(mock_post):
- """Test async execution returns AsyncExecutionResult."""
- mock_response = Mock()
- mock_response.ok = True
- mock_response.status_code = 202
- mock_response.json.return_value = {
- "data": {
- "runId": "execution-123",
- "statusUrl": "https://www.sim.ai/api/v2/workflows/workflow-id/runs/execution-123"
- }
- }
- mock_response.headers.get.return_value = None
- mock_post.return_value = mock_response
-
- client = SimStudioClient(api_key="test-api-key")
- result = client.execute_workflow(
- "workflow-id",
- {"message": "Hello"},
- async_execution=True
- )
-
- assert result.success is True
- assert result.run_id == "execution-123"
- assert result.status_url == "https://www.sim.ai/api/v2/workflows/workflow-id/runs/execution-123"
- assert result.async_execution is True
-
- call_args = mock_post.call_args
- assert call_args.args[0] == "https://www.sim.ai/api/v2/workflows/workflow-id/execute"
- assert "X-Execution-Mode" not in call_args.kwargs["headers"]
- assert call_args.kwargs["json"] == {
- "input": {"message": "Hello"},
- "async": True
- }
-
-
-@patch('simstudio.requests.Session.post')
-def test_sync_execution_returns_result(mock_post):
- """Test sync execution returns WorkflowExecutionResult."""
- mock_response = Mock()
- mock_response.ok = True
- mock_response.status_code = 200
- mock_response.json.return_value = v2_execution_response({"result": "completed"})
- mock_response.headers.get.return_value = None
- mock_post.return_value = mock_response
-
- client = SimStudioClient(api_key="test-api-key")
- result = client.execute_workflow(
- "workflow-id",
- {"message": "Hello"},
- async_execution=False
- )
-
- assert result.success is True
- assert result.status == "completed"
- assert result.output == {"result": "completed"}
- assert result.metadata == {
- "duration": 10,
- "runId": "execution-123",
- "startTime": "2026-08-11T12:00:00.000Z",
- "endTime": "2026-08-11T12:00:00.010Z"
- }
- assert not hasattr(result, 'task_id')
-
-
-@patch('simstudio.requests.Session.post')
-def test_sync_execution_cancelled_is_not_success(mock_post):
- """A run cancelled out of band is not a success, matching the TypeScript SDK."""
- mock_execution_post(mock_post, status="cancelled")
-
- client = SimStudioClient(api_key="test-api-key")
- result = client.execute_workflow("workflow-id", {})
-
- assert result.success is False
- assert result.status == "cancelled"
-
-
-@patch('simstudio.requests.Session.post')
-def test_sync_execution_failed_is_not_success(mock_post):
- """A failed run is not a success and surfaces the server's error message."""
- mock_execution_post(
- mock_post,
- status="failed",
- error={"code": "BLOCK_EXECUTION_FAILED", "message": "Invalid credentials"}
- )
-
- client = SimStudioClient(api_key="test-api-key")
- result = client.execute_workflow("workflow-id", {})
-
- assert result.success is False
- assert result.status == "failed"
- assert result.error == "Invalid credentials"
-
-
-@patch('simstudio.requests.Session.post')
-def test_sync_execution_paused_is_success(mock_post):
- """A paused run is still a success -- it is waiting, not broken."""
- mock_execution_post(mock_post, status="paused")
-
- client = SimStudioClient(api_key="test-api-key")
- result = client.execute_workflow("workflow-id", {})
-
- assert result.success is True
- assert result.status == "paused"
-
-
-@patch('simstudio.requests.Session.post')
-def test_async_header_not_set_when_false(mock_post):
- """Test X-Execution-Mode header is not set when async_execution is None."""
- mock_response = Mock()
- mock_response.ok = True
- mock_response.status_code = 200
- mock_response.json.return_value = v2_execution_response()
- mock_response.headers.get.return_value = None
- mock_post.return_value = mock_response
-
- client = SimStudioClient(api_key="test-api-key")
- client.execute_workflow("workflow-id", {"message": "Hello"})
-
- call_args = mock_post.call_args
- assert "X-Execution-Mode" not in call_args[1]["headers"]
-
-
-@patch('simstudio.requests.Session.post')
-def test_async_execution_timeout_body(mock_post):
- mock_response = Mock()
- mock_response.ok = True
- mock_response.status_code = 202
- mock_response.json.return_value = {
- "data": {
- "runId": "execution-123",
- "statusUrl": "/api/v2/workflows/workflow-id/runs/execution-123",
- }
- }
- mock_response.headers.get.return_value = None
- mock_post.return_value = mock_response
-
- client = SimStudioClient(api_key="test-api-key")
- client.execute_workflow(
- "workflow-id",
- {},
- async_execution=True,
- execution_timeout_seconds=90,
- )
-
- body = mock_post.call_args[1]["json"]
- assert body["executionTimeoutSeconds"] == 90
-
-
-def test_sync_execution_rejects_execution_timeout():
- client = SimStudioClient(api_key="test-api-key")
-
- with pytest.raises(SimStudioError) as exc_info:
- client.execute_workflow("workflow-id", {}, execution_timeout_seconds=90)
-
- assert exc_info.value.code == "INVALID_EXECUTION_TIMEOUT"
-
-
-def test_execution_timeout_rejects_more_than_seven_days():
- client = SimStudioClient(api_key="test-api-key")
-
- with pytest.raises(SimStudioError) as exc_info:
- client.execute_workflow(
- "workflow-id",
- {},
- async_execution=True,
- execution_timeout_seconds=604_801,
- )
-
- assert exc_info.value.code == "INVALID_EXECUTION_TIMEOUT"
-
-
-def test_execute_with_retry_forwards_execution_timeout():
- client = SimStudioClient(api_key="test-api-key")
- expected = Mock()
-
- with patch.object(client, "execute_workflow", return_value=expected) as execute_workflow:
- result = client.execute_with_retry(
- "workflow-id",
- {"message": "hello"},
- async_execution=True,
- execution_timeout_seconds=90,
- )
-
- assert result is expected
- execute_workflow.assert_called_once_with(
- "workflow-id",
- {"message": "hello"},
- timeout=30.0,
- stream=None,
- selected_outputs=None,
- async_execution=True,
- execution_timeout_seconds=90,
- )
-
-
-@patch('simstudio.requests.Session.get')
-def test_get_job_status_success(mock_get):
- """Test getting legacy job status."""
- mock_response = Mock()
- mock_response.ok = True
- mock_response.json.return_value = {
- "success": True,
- "taskId": "task-123",
- "status": "completed",
- "metadata": {"duration": 60000},
- "output": {"result": "done"}
- }
- mock_response.headers.get.return_value = None
- mock_get.return_value = mock_response
-
- client = SimStudioClient(api_key="test-api-key", base_url="https://test.sim.ai")
- result = client.get_job_status("task-123")
-
- assert result["taskId"] == "task-123"
- assert result["status"] == "completed"
- assert result["output"]["result"] == "done"
- mock_get.assert_called_once_with("https://test.sim.ai/api/jobs/task-123")
-
-
-@patch('simstudio.requests.Session.get')
-def test_get_job_status_not_found(mock_get):
- """Test legacy job not found error."""
- mock_response = Mock()
- mock_response.ok = False
- mock_response.status_code = 404
- mock_response.reason = "Not Found"
- mock_response.json.return_value = {
- "error": "Job not found",
- "code": "JOB_NOT_FOUND"
- }
- mock_response.headers.get.return_value = None
- mock_get.return_value = mock_response
-
- client = SimStudioClient(api_key="test-api-key")
-
- with pytest.raises(SimStudioError) as exc_info:
- client.get_job_status("invalid-task")
- assert "Job not found" in str(exc_info.value)
-
-
-@patch('simstudio.requests.Session.get')
-def test_get_workflow_run_success(mock_get):
- mock_response = Mock()
- mock_response.ok = True
- mock_response.json.return_value = {
- "data": {
- "runId": "execution-123",
- "workflowId": "workflow-123",
- "status": "completed",
- "output": {"result": "done"}
- }
- }
- mock_response.headers.get.return_value = None
- mock_get.return_value = mock_response
-
- client = SimStudioClient(api_key="test-api-key", base_url="https://test.sim.ai")
- result = client.get_workflow_run(
- "workflow-123",
- "execution-123",
- include_output=True,
- selected_outputs=["agent.content"]
- )
-
- assert result["runId"] == "execution-123"
- assert result["status"] == "completed"
- assert result["output"]["result"] == "done"
- mock_get.assert_called_once_with(
- "https://test.sim.ai/api/v2/workflows/workflow-123/runs/execution-123",
- params={"includeOutput": "true", "selectedOutputs": "agent.content"}
- )
-
-
-@patch('simstudio.requests.Session.get')
-def test_get_workflow_run_not_found(mock_get):
- mock_response = Mock()
- mock_response.ok = False
- mock_response.status_code = 404
- mock_response.reason = "Not Found"
- mock_response.json.return_value = {
- "error": {
- "code": "NOT_FOUND",
- "message": "Run not found"
- }
- }
- mock_response.headers.get.return_value = None
- mock_get.return_value = mock_response
-
- client = SimStudioClient(api_key="test-api-key")
-
- with pytest.raises(SimStudioError) as exc_info:
- client.get_workflow_run("workflow-123", "invalid-run")
- assert "Run not found" in str(exc_info.value)
-
-
-@patch('simstudio.requests.Session.post')
-@patch('simstudio.time.sleep')
-def test_execute_with_retry_success_first_attempt(mock_sleep, mock_post):
- """Test retry succeeds on first attempt."""
- mock_response = Mock()
- mock_response.ok = True
- mock_response.status_code = 200
- mock_response.json.return_value = v2_execution_response({"result": "success"})
- mock_response.headers.get.return_value = None
- mock_post.return_value = mock_response
-
- client = SimStudioClient(api_key="test-api-key")
- result = client.execute_with_retry("workflow-id", {"message": "test"})
-
- assert result.success is True
- assert mock_post.call_count == 1
- assert mock_sleep.call_count == 0
-
-
-@patch('simstudio.requests.Session.post')
-@patch('simstudio.time.sleep')
-def test_execute_with_retry_retries_on_rate_limit(mock_sleep, mock_post):
- """Test retry retries on rate limit error."""
- rate_limit_response = Mock()
- rate_limit_response.ok = False
- rate_limit_response.status_code = 429
- rate_limit_response.json.return_value = {
- "error": "Rate limit exceeded",
- "code": "RATE_LIMIT_EXCEEDED"
- }
- import time
- rate_limit_response.headers.get.side_effect = lambda h: {
- 'retry-after': '1',
- 'x-ratelimit-limit': '100',
- 'x-ratelimit-remaining': '0',
- 'x-ratelimit-reset': str(int(time.time()) + 60)
- }.get(h)
-
- success_response = Mock()
- success_response.ok = True
- success_response.status_code = 200
- success_response.json.return_value = v2_execution_response({"result": "success"})
- success_response.headers.get.return_value = None
-
- mock_post.side_effect = [rate_limit_response, success_response]
-
- client = SimStudioClient(api_key="test-api-key")
- result = client.execute_with_retry(
- "workflow-id",
- {"message": "test"},
- max_retries=3,
- initial_delay=0.01
- )
-
- assert result.success is True
- assert mock_post.call_count == 2
- assert mock_sleep.call_count == 1
-
-
-@patch('simstudio.requests.Session.post')
-@patch('simstudio.time.sleep')
-def test_execute_with_retry_max_retries_exceeded(mock_sleep, mock_post):
- """Test retry throws after max retries."""
- mock_response = Mock()
- mock_response.ok = False
- mock_response.status_code = 429
- mock_response.json.return_value = {
- "error": "Rate limit exceeded",
- "code": "RATE_LIMIT_EXCEEDED"
- }
- mock_response.headers.get.side_effect = lambda h: '1' if h == 'retry-after' else None
- mock_post.return_value = mock_response
-
- client = SimStudioClient(api_key="test-api-key")
-
- with pytest.raises(SimStudioError) as exc_info:
- client.execute_with_retry(
- "workflow-id",
- {"message": "test"},
- max_retries=2,
- initial_delay=0.01
- )
-
- assert "Rate limit exceeded" in str(exc_info.value)
- assert mock_post.call_count == 3 # Initial + 2 retries
-
-
-@patch('simstudio.requests.Session.post')
-def test_execute_with_retry_no_retry_on_other_errors(mock_post):
- """Test retry does not retry on non-rate-limit errors."""
- mock_response = Mock()
- mock_response.ok = False
- mock_response.status_code = 500
- mock_response.reason = "Internal Server Error"
- mock_response.json.return_value = {
- "error": {
- "code": "INTERNAL_ERROR",
- "message": "Server error"
- }
- }
- mock_response.headers.get.return_value = None
- mock_post.return_value = mock_response
-
- client = SimStudioClient(api_key="test-api-key")
-
- with pytest.raises(SimStudioError) as exc_info:
- client.execute_with_retry("workflow-id", {"message": "test"})
-
- assert "Server error" in str(exc_info.value)
- assert mock_post.call_count == 1 # No retries
-
-
-def test_get_rate_limit_info_returns_none_initially():
- """Test rate limit info is None before any API calls."""
- client = SimStudioClient(api_key="test-api-key")
- info = client.get_rate_limit_info()
- assert info is None
-
-
-@patch('simstudio.requests.Session.post')
-def test_get_rate_limit_info_after_api_call(mock_post):
- """Test rate limit info is populated after API call."""
- mock_response = Mock()
- mock_response.ok = True
- mock_response.status_code = 200
- mock_response.json.return_value = v2_execution_response()
- mock_response.headers.get.side_effect = lambda h: {
- 'x-ratelimit-limit': '100',
- 'x-ratelimit-remaining': '95',
- 'x-ratelimit-reset': '1704067200'
- }.get(h)
- mock_post.return_value = mock_response
-
- client = SimStudioClient(api_key="test-api-key")
- client.execute_workflow("workflow-id", {})
-
- info = client.get_rate_limit_info()
- assert info is not None
- assert info.limit == 100
- assert info.remaining == 95
- assert info.reset == 1704067200
-
-
-@patch('simstudio.requests.Session.post')
-def test_rate_limit_reset_accepts_iso_timestamp(mock_post):
- """The v2 API sends X-RateLimit-Reset as an ISO 8601 timestamp, not an epoch int."""
- mock_execution_post(mock_post, headers={
- 'x-ratelimit-limit': '100',
- 'x-ratelimit-remaining': '99',
- 'x-ratelimit-reset': '2024-01-01T00:00:00.000Z'
- })
-
- client = SimStudioClient(api_key="test-api-key")
- result = client.execute_workflow("workflow-id", {})
-
- assert result.success is True
- info = client.get_rate_limit_info()
- assert info is not None
- assert info.reset == 1704067200000
-
-
-@pytest.mark.parametrize('reset_header', ['not-a-timestamp', '²'])
-@patch('simstudio.requests.Session.post')
-def test_rate_limit_reset_tolerates_unparseable_value(mock_post, reset_header):
- """
- An unrecognised quota hint reports 0 rather than failing the execution.
-
- '²' covers the digit-like characters str.isdigit() accepts but int()
- rejects.
- """
- mock_execution_post(mock_post, headers={
- 'x-ratelimit-limit': '100',
- 'x-ratelimit-remaining': '99',
- 'x-ratelimit-reset': reset_header
- })
-
- client = SimStudioClient(api_key="test-api-key")
- result = client.execute_workflow("workflow-id", {})
-
- assert result.success is True
- info = client.get_rate_limit_info()
- assert info is not None
- assert info.reset == 0
-
-
-@patch('simstudio.requests.Session.get')
-def test_get_usage_limits_success(mock_get):
- """Test getting usage limits."""
- mock_response = Mock()
- mock_response.ok = True
- mock_response.json.return_value = {
- "success": True,
- "rateLimit": {
- "sync": {
- "isLimited": False,
- "limit": 100,
- "remaining": 95,
- "resetAt": "2024-01-01T01:00:00Z"
- },
- "async": {
- "isLimited": False,
- "limit": 50,
- "remaining": 48,
- "resetAt": "2024-01-01T01:00:00Z"
- },
- "authType": "api"
- },
- "usage": {
- "currentPeriodCost": 1.23,
- "limit": 100.0,
- "plan": "pro"
- }
- }
- mock_response.headers.get.return_value = None
- mock_get.return_value = mock_response
-
- client = SimStudioClient(api_key="test-api-key", base_url="https://test.sim.ai")
- result = client.get_usage_limits()
-
- assert result.success is True
- assert result.rate_limit["sync"]["limit"] == 100
- assert result.rate_limit["async"]["limit"] == 50
- assert result.usage["currentPeriodCost"] == 1.23
- assert result.usage["plan"] == "pro"
- mock_get.assert_called_once_with("https://test.sim.ai/api/users/me/usage-limits")
-
-
-@patch('simstudio.requests.Session.get')
-def test_get_usage_limits_unauthorized(mock_get):
- """Test usage limits with invalid API key."""
- mock_response = Mock()
- mock_response.ok = False
- mock_response.status_code = 401
- mock_response.reason = "Unauthorized"
- mock_response.json.return_value = {
- "error": "Invalid API key",
- "code": "UNAUTHORIZED"
- }
- mock_response.headers.get.return_value = None
- mock_get.return_value = mock_response
-
- client = SimStudioClient(api_key="invalid-key")
-
- with pytest.raises(SimStudioError) as exc_info:
- client.get_usage_limits()
- assert "Invalid API key" in str(exc_info.value)
-
-
-@patch('simstudio.requests.Session.post')
-def test_execute_workflow_with_stream_and_selected_outputs(mock_post):
- """Test execution with stream and selectedOutputs parameters."""
- mock_response = Mock()
- mock_response.ok = True
- mock_response.status_code = 200
- mock_response.json.return_value = v2_execution_response()
- mock_response.headers.get.return_value = None
- mock_post.return_value = mock_response
-
- client = SimStudioClient(api_key="test-api-key")
- client.execute_workflow(
- "workflow-id",
- {"message": "test"},
- stream=True,
- selected_outputs=["agent1.content", "agent2.content"]
- )
-
- call_args = mock_post.call_args
- request_body = call_args[1]["json"]
-
- assert request_body["input"] == {"message": "test"}
- assert request_body["stream"] is True
- assert request_body["selectedOutputs"] == ["agent1.content", "agent2.content"]
-
-
-# Tests for primitive and list inputs
-@patch('simstudio.requests.Session.post')
-def test_execute_workflow_with_string_input(mock_post):
- """Test execution with primitive string input wraps in input field."""
- mock_response = Mock()
- mock_response.ok = True
- mock_response.status_code = 200
- mock_response.json.return_value = v2_execution_response()
- mock_response.headers.get.return_value = None
- mock_post.return_value = mock_response
-
- client = SimStudioClient(api_key="test-api-key")
- client.execute_workflow("workflow-id", "NVDA")
-
- call_args = mock_post.call_args
- request_body = call_args[1]["json"]
-
- assert request_body["input"] == {"input": "NVDA"}
- assert "0" not in request_body # Should not spread string characters
-
-
-@patch('simstudio.requests.Session.post')
-def test_execute_workflow_with_number_input(mock_post):
- """Test execution with primitive number input wraps in input field."""
- mock_response = Mock()
- mock_response.ok = True
- mock_response.status_code = 200
- mock_response.json.return_value = v2_execution_response()
- mock_response.headers.get.return_value = None
- mock_post.return_value = mock_response
-
- client = SimStudioClient(api_key="test-api-key")
- client.execute_workflow("workflow-id", 42)
-
- call_args = mock_post.call_args
- request_body = call_args[1]["json"]
-
- assert request_body["input"] == {"input": 42}
-
-
-@patch('simstudio.requests.Session.post')
-def test_execute_workflow_with_list_input(mock_post):
- """Test execution with list input wraps in input field."""
- mock_response = Mock()
- mock_response.ok = True
- mock_response.status_code = 200
- mock_response.json.return_value = v2_execution_response()
- mock_response.headers.get.return_value = None
- mock_post.return_value = mock_response
-
- client = SimStudioClient(api_key="test-api-key")
- client.execute_workflow("workflow-id", ["NVDA", "AAPL", "GOOG"])
-
- call_args = mock_post.call_args
- request_body = call_args[1]["json"]
-
- assert request_body["input"] == {"input": ["NVDA", "AAPL", "GOOG"]}
- assert "0" not in request_body # Should not spread list
-
-
-@patch('simstudio.requests.Session.post')
-def test_execute_workflow_with_dict_input_uses_v2_input_field(mock_post):
- mock_response = Mock()
- mock_response.ok = True
- mock_response.status_code = 200
- mock_response.json.return_value = v2_execution_response()
- mock_response.headers.get.return_value = None
- mock_post.return_value = mock_response
-
- client = SimStudioClient(api_key="test-api-key")
- client.execute_workflow("workflow-id", {"ticker": "NVDA", "quantity": 100})
-
- call_args = mock_post.call_args
- request_body = call_args[1]["json"]
-
- assert request_body["input"] == {"ticker": "NVDA", "quantity": 100}
-
-
-def test_identifies_the_sdk_on_every_request():
- client = SimStudioClient(api_key="test-key")
- python_version = platform.python_version()
-
- assert client._session.headers["X-Sim-Client-Info"] == f"sdk-python/{__version__}; python/{python_version}"
- assert client._session.headers["User-Agent"] == f"simstudio-python-sdk/{__version__} python/{python_version}"
- assert client._session.headers["X-API-Key"] == "test-key"
diff --git a/packages/ts-sdk/.gitignore b/packages/ts-sdk/.gitignore
deleted file mode 100644
index dcb35fa34e8..00000000000
--- a/packages/ts-sdk/.gitignore
+++ /dev/null
@@ -1,43 +0,0 @@
-# Dependencies
-node_modules/
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
-
-# Build output
-dist/
-build/
-
-# Coverage reports
-coverage/
-*.lcov
-
-# Cache directories
-.turbo/
-.cache/
-.parcel-cache/
-
-# Environment variables
-.env
-.env.local
-.env.development.local
-.env.test.local
-.env.production.local
-
-# IDE
-.vscode/
-.idea/
-*.swp
-*.swo
-
-# OS
-.DS_Store
-Thumbs.db
-
-# TypeScript
-*.tsbuildinfo
-
-# Logs
-logs
-*.log
\ No newline at end of file
diff --git a/packages/ts-sdk/README.md b/packages/ts-sdk/README.md
deleted file mode 100644
index 8357f8d68c9..00000000000
--- a/packages/ts-sdk/README.md
+++ /dev/null
@@ -1,575 +0,0 @@
-# Sim TypeScript SDK
-
-The official TypeScript/JavaScript SDK for [Sim](https://sim.ai), allowing you to execute workflows programmatically from your applications.
-
-## Server compatibility
-
-`0.2.x` talks to the v2 API and has no fallback to the older endpoints, so it requires a Sim deployment that serves `POST /api/v2/workflows/{id}/execute`. That surface is newer than the endpoints `0.1.x` used. If it is unavailable, `executeWorkflow` fails with `HTTP 404: Not Found` — upgrade the server or stay on `simstudio-ts-sdk@0.1.x`, which keeps using `/api/workflows/{id}/execute` and `/api/jobs/{id}`.
-
-## Upgrading from 0.1.x to 0.2.0
-
-`0.2.0` is a breaking release. It is a minor bump rather than a patch precisely so that `^0.1.2` does not pick it up — you upgrade when you choose to.
-
-- **Requests move to `/api/v2`.** `executeWorkflow` posts to `/api/v2/workflows/{id}/execute`, sends the workflow input nested under `input`, and carries `async` / `executionTimeoutSeconds` in the body instead of the `X-Execution-Mode` and `X-Execution-Timeout-Seconds` headers.
-- **`AsyncExecutionResult.jobId` is now `runId`,** and `executionId` has been removed from that interface. Replace `result.jobId` with `result.runId`.
-- **`getJobStatus(taskId)` is legacy.** It still calls `/api/jobs/{taskId}` and only resolves IDs from a `0.1.x` async execution. For runs started by `0.2.x`, use `getWorkflowRun(workflowId, runId)`, which reads `/api/v2/workflows/{id}/runs/{runId}` and returns a typed `WorkflowRunStatus`.
-- **A failed synchronous run now throws.** Previously it resolved with `{ success: false }`; it now rejects with a `SimStudioError` carrying the server's `error.code` and `error.message`. Any `if (!result.success)` branch that handled failures must move into a `catch`.
-- **`success` is derived from the run status.** It is `true` for `completed` and `paused` runs only, so a run cancelled while it was in flight resolves with `success: false` rather than throwing. Combined with the point above: a rejection means the run failed, and a resolved `success: false` means it was cancelled.
-
-## Installation
-
-```bash
-npm install simstudio-ts-sdk
-# or
-yarn add simstudio-ts-sdk
-# or
-bun add simstudio-ts-sdk
-```
-
-## Quick Start
-
-```typescript
-import { SimStudioClient } from 'simstudio-ts-sdk';
-
-// Initialize the client
-const client = new SimStudioClient({
- apiKey: 'your-api-key-here',
- baseUrl: 'https://www.sim.ai' // optional, defaults to https://www.sim.ai
-});
-
-// Execute a workflow
-try {
- const result = await client.executeWorkflow('workflow-id');
- console.log('Workflow executed successfully:', result);
-} catch (error) {
- console.error('Workflow execution failed:', error);
-}
-```
-
-## API Reference
-
-### SimStudioClient
-
-#### Constructor
-
-```typescript
-new SimStudioClient(config: SimStudioConfig)
-```
-
-- `config.apiKey` (string): Your Sim API key
-- `config.baseUrl` (string, optional): Base URL for the Sim API (defaults to `https://www.sim.ai`)
-
-#### Methods
-
-##### executeWorkflow(workflowId, input?, options?)
-
-Execute a workflow with optional input data.
-
-```typescript
-// With object input (sent as the v2 input object)
-const result = await client.executeWorkflow('workflow-id', {
- message: 'Hello, world!'
-});
-
-// With primitive input (sent as { input: { input: value } })
-const result = await client.executeWorkflow('workflow-id', 'NVDA');
-
-// With options
-const result = await client.executeWorkflow('workflow-id', { message: 'Hello' }, {
- timeout: 60000,
- async: true,
- executionTimeoutSeconds: 3600
-});
-```
-
-**Parameters:**
-- `workflowId` (string): The ID of the workflow to execute
-- `input` (any, optional): Input data to pass to the workflow. Objects become the v2 `input` object; primitives and arrays become `{ input: value }` inside it. File objects are automatically converted to base64.
-- `options` (ExecutionOptions, optional):
- - `timeout` (number): Timeout in milliseconds (default: 30000)
- - `stream` (boolean): Enable streaming responses
- - `selectedOutputs` (string[]): Block outputs to stream (e.g., `["agent1.content"]`)
- - `async` (boolean): Execute asynchronously and return a run ID
- - `executionTimeoutSeconds` (number): Server-side async execution cap from 1 to 604800 seconds. Requires `async: true` and cannot extend the account policy.
-
-**Returns:** `Promise`
-
-Synchronous executions that finish with `status: 'failed'` reject with `SimStudioError`.
-
-##### getWorkflowStatus(workflowId)
-
-Get the status of a workflow (deployment status, etc.).
-
-```typescript
-const status = await client.getWorkflowStatus('workflow-id');
-console.log('Is deployed:', status.isDeployed);
-```
-
-**Parameters:**
-- `workflowId` (string): The ID of the workflow
-
-**Returns:** `Promise`
-
-##### validateWorkflow(workflowId)
-
-Validate that a workflow is ready for execution.
-
-```typescript
-const isReady = await client.validateWorkflow('workflow-id');
-if (isReady) {
- // Workflow is deployed and ready
-}
-```
-
-**Parameters:**
-- `workflowId` (string): The ID of the workflow
-
-**Returns:** `Promise`
-
-##### executeWorkflowSync(workflowId, input?, options?)
-
-Execute a workflow and poll for completion (useful for long-running workflows).
-
-```typescript
-const result = await client.executeWorkflowSync('workflow-id', { data: 'some input' }, {
- timeout: 60000
-});
-```
-
-**Parameters:**
-- `workflowId` (string): The ID of the workflow to execute
-- `input` (any, optional): Input data to pass to the workflow
-- `options` (ExecutionOptions, optional):
- - `timeout` (number): Timeout for the initial request in milliseconds
-
-**Returns:** `Promise`
-
-##### getWorkflowRun(workflowId, runId, options?)
-
-Get the status and optional outputs of a workflow run. Use the `runId` returned by async execution.
-
-```typescript
-const status = await client.getWorkflowRun('workflow-id', 'run-id', {
- includeOutput: true,
- selectedOutputs: ['agent.content']
-});
-console.log('Run status:', status.status);
-```
-
-**Parameters:**
-- `workflowId` (string): The workflow ID
-- `runId` (string): The run ID returned from async execution
-- `options.includeOutput` (boolean, optional): Include the final output for completed executions
-- `options.selectedOutputs` (string[], optional): Block output selectors to include
-
-**Returns:** `Promise`
-
-##### getJobStatus(jobId)
-
-Get the status of a job created through the legacy async execution endpoint. New integrations should use `getWorkflowRun()` with a run ID.
-
-```typescript
-const status = await client.getJobStatus('legacy-job-id');
-```
-
-**Returns:** `Promise`
-
-##### executeWithRetry(workflowId, input?, options?, retryOptions?)
-
-Execute a workflow with automatic retry on rate limit errors.
-
-```typescript
-const result = await client.executeWithRetry('workflow-id', { message: 'Hello' }, {
- timeout: 30000
-}, {
- maxRetries: 3,
- initialDelay: 1000,
- maxDelay: 30000,
- backoffMultiplier: 2
-});
-```
-
-**Parameters:**
-- `workflowId` (string): The ID of the workflow to execute
-- `input` (any, optional): Input data to pass to the workflow
-- `options` (ExecutionOptions, optional): Execution options
-- `retryOptions` (RetryOptions, optional):
- - `maxRetries` (number): Maximum retry attempts (default: 3)
- - `initialDelay` (number): Initial delay in ms (default: 1000)
- - `maxDelay` (number): Maximum delay in ms (default: 30000)
- - `backoffMultiplier` (number): Backoff multiplier (default: 2)
-
-**Returns:** `Promise`
-
-##### getRateLimitInfo()
-
-Get current rate limit information from the last API response.
-
-```typescript
-const rateInfo = client.getRateLimitInfo();
-if (rateInfo) {
- console.log('Remaining requests:', rateInfo.remaining);
-}
-```
-
-**Returns:** `RateLimitInfo | null`
-
-##### getUsageLimits()
-
-Get current usage limits and quota information.
-
-```typescript
-const limits = await client.getUsageLimits();
-console.log('Current usage:', limits.usage);
-```
-
-**Returns:** `Promise`
-
-##### setApiKey(apiKey)
-
-Update the API key.
-
-```typescript
-client.setApiKey('new-api-key');
-```
-
-##### setBaseUrl(baseUrl)
-
-Update the base URL.
-
-```typescript
-client.setBaseUrl('https://my-custom-domain.com');
-```
-
-## Types
-
-### WorkflowExecutionResult
-
-```typescript
-interface WorkflowExecutionResult {
- success: boolean;
- executionId?: string;
- output?: any;
- error?: string;
- logs?: any[];
- metadata?: {
- duration?: number;
- executionId?: string;
- runId?: string;
- startTime?: string;
- endTime?: string;
- [key: string]: any;
- };
- traceSpans?: any[];
- totalDuration?: number;
-}
-```
-
-### LargeValueRef
-
-Oversized execution values may be returned as a versioned reference inside `output`, `logs`, streaming events, or execution status responses.
-The `key` field is an opaque execution-scoped server storage pointer, not a client-readable download URL.
-
-```typescript
-interface LargeValueRef {
- __simLargeValueRef: true;
- version: 1;
- id: string;
- kind: 'array' | 'object' | 'string' | 'json';
- size: number;
- key?: string;
- executionId?: string;
- preview?: unknown;
-}
-```
-
-### WorkflowStatus
-
-```typescript
-interface WorkflowStatus {
- isDeployed: boolean;
- deployedAt?: string;
- needsRedeployment: boolean;
-}
-```
-
-### SimStudioError
-
-```typescript
-class SimStudioError extends Error {
- code?: string;
- status?: number;
-}
-```
-
-### AsyncExecutionResult
-
-```typescript
-interface AsyncExecutionResult {
- success: boolean;
- runId: string;
- statusUrl: string;
- message: string;
- async: true;
-}
-```
-
-### RateLimitInfo
-
-```typescript
-interface RateLimitInfo {
- limit: number;
- remaining: number;
- reset: number;
- retryAfter?: number;
-}
-```
-
-### UsageLimits
-
-```typescript
-interface UsageLimits {
- success: boolean;
- rateLimit: {
- sync: {
- isLimited: boolean;
- limit: number;
- remaining: number;
- resetAt: string;
- };
- async: {
- isLimited: boolean;
- limit: number;
- remaining: number;
- resetAt: string;
- };
- authType: string;
- };
- usage: {
- currentPeriodCost: number;
- limit: number;
- plan: string;
- };
-}
-```
-
-### ExecutionOptions
-
-```typescript
-interface ExecutionOptions {
- timeout?: number;
- stream?: boolean;
- selectedOutputs?: string[];
- async?: boolean;
- executionTimeoutSeconds?: number;
-}
-```
-
-### RetryOptions
-
-```typescript
-interface RetryOptions {
- maxRetries?: number;
- initialDelay?: number;
- maxDelay?: number;
- backoffMultiplier?: number;
-}
-```
-
-## Examples
-
-### Basic Workflow Execution
-
-```typescript
-import { SimStudioClient } from 'simstudio-ts-sdk';
-
-const client = new SimStudioClient({
- apiKey: process.env.SIM_API_KEY!
-});
-
-async function runWorkflow() {
- try {
- // Check if workflow is ready
- const isReady = await client.validateWorkflow('my-workflow-id');
- if (!isReady) {
- throw new Error('Workflow is not deployed or ready');
- }
-
- // Execute the workflow
- const result = await client.executeWorkflow('my-workflow-id', {
- message: 'Process this data',
- userId: '12345'
- });
-
- if (result.success) {
- console.log('Output:', result.output);
- console.log('Duration:', result.metadata?.duration);
- } else {
- console.error('Workflow failed:', result.error);
- }
- } catch (error) {
- console.error('Error:', error);
- }
-}
-
-runWorkflow();
-```
-
-### Error Handling
-
-```typescript
-import { SimStudioClient, SimStudioError } from 'simstudio-ts-sdk';
-
-const client = new SimStudioClient({
- apiKey: process.env.SIM_API_KEY!
-});
-
-async function executeWithErrorHandling() {
- try {
- const result = await client.executeWorkflow('workflow-id');
- return result;
- } catch (error) {
- if (error instanceof SimStudioError) {
- switch (error.code) {
- case 'UNAUTHORIZED':
- console.error('Invalid API key');
- break;
- case 'TIMEOUT':
- console.error('Workflow execution timed out');
- break;
- case 'USAGE_LIMIT_EXCEEDED':
- console.error('Usage limit exceeded');
- break;
- case 'INVALID_JSON':
- console.error('Invalid JSON in request body');
- break;
- default:
- console.error('Workflow error:', error.message);
- }
- } else {
- console.error('Unexpected error:', error);
- }
- throw error;
- }
-}
-```
-
-### Environment Configuration
-
-```typescript
-// Using environment variables
-const client = new SimStudioClient({
- apiKey: process.env.SIM_API_KEY!,
- baseUrl: process.env.SIM_BASE_URL // optional
-});
-```
-
-### File Upload
-
-File objects are automatically detected and converted to base64 format. Include them in your input under the field name matching your workflow's API trigger input format:
-
-The SDK converts File objects to this format:
-```typescript
-{
- type: 'file',
- data: 'data:mime/type;base64,base64data',
- name: 'filename',
- mime: 'mime/type'
-}
-```
-
-Alternatively, you can manually provide files using the URL format:
-```typescript
-{
- type: 'url',
- data: 'https://example.com/file.pdf',
- name: 'file.pdf',
- mime: 'application/pdf'
-}
-```
-
-```typescript
-import { SimStudioClient } from 'simstudio-ts-sdk';
-import fs from 'fs';
-
-const client = new SimStudioClient({
- apiKey: process.env.SIM_API_KEY!
-});
-
-// Node.js: Read file and create File object
-const fileBuffer = fs.readFileSync('./document.pdf');
-const file = new File([fileBuffer], 'document.pdf', { type: 'application/pdf' });
-
-// Include files under the field name from your API trigger's input format
-const result = await client.executeWorkflow('workflow-id', {
- documents: [file], // Field name must match your API trigger's file input field
- instructions: 'Process this document'
-});
-
-// Browser: From file input
-const handleFileUpload = async (event: Event) => {
- const inputEl = event.target as HTMLInputElement;
- const files = Array.from(inputEl.files || []);
-
- const result = await client.executeWorkflow('workflow-id', {
- attachments: files, // Field name must match your API trigger's file input field
- query: 'Analyze these files'
- });
-};
-```
-
-## Getting Your API Key
-
-1. Log in to your [Sim](https://sim.ai) account
-2. Navigate to your workflow
-3. Click on "Deploy" to deploy your workflow
-4. Select or create an API key during the deployment process
-5. Copy the API key to use in your application
-
-## Development
-
-### Running Tests
-
-To run the tests locally:
-
-1. Clone the repository and navigate to the TypeScript SDK directory:
- ```bash
- cd packages/ts-sdk
- ```
-
-2. Install dependencies:
- ```bash
- bun install
- ```
-
-3. Run the tests:
- ```bash
- bun run test
- ```
-
-### Building
-
-Build the TypeScript SDK:
-
-```bash
-bun run build
-```
-
-This will compile TypeScript files to JavaScript and generate type declarations in the `dist/` directory.
-
-### Development Mode
-
-For development with auto-rebuild:
-
-```bash
-bun run dev
-```
-
-## Requirements
-
-- Node.js 18+
-- TypeScript 5.0+ (for TypeScript projects)
-
-## License
-
-Apache-2.0
diff --git a/packages/ts-sdk/examples/basic-usage.ts b/packages/ts-sdk/examples/basic-usage.ts
deleted file mode 100644
index f5c024ab892..00000000000
--- a/packages/ts-sdk/examples/basic-usage.ts
+++ /dev/null
@@ -1,164 +0,0 @@
-import { SimStudioClient, SimStudioError } from '../src/index'
-
-// Example 1: Basic workflow execution
-async function basicExample() {
- const client = new SimStudioClient({
- apiKey: process.env.SIM_API_KEY!,
- baseUrl: 'https://www.sim.ai',
- })
-
- try {
- // Execute a workflow without input
- const result = await client.executeWorkflow('your-workflow-id')
-
- if (result.success) {
- console.log('✅ Workflow executed successfully!')
- console.log('Output:', result.output)
- console.log('Duration:', result.metadata?.duration, 'ms')
- } else {
- console.log('❌ Workflow failed:', result.error)
- }
- } catch (error) {
- if (error instanceof SimStudioError) {
- console.error('SDK Error:', error.message, 'Code:', error.code)
- } else {
- console.error('Unexpected error:', error)
- }
- }
-}
-
-// Example 2: Workflow execution with input data
-async function withInputExample() {
- const client = new SimStudioClient({
- apiKey: process.env.SIM_API_KEY!,
- })
-
- try {
- const result = await client.executeWorkflow('your-workflow-id', {
- input: {
- message: 'Hello from SDK!',
- userId: '12345',
- data: {
- type: 'analysis',
- parameters: {
- includeMetadata: true,
- format: 'json',
- },
- },
- },
- timeout: 60000, // 60 seconds
- })
-
- if (result.success) {
- console.log('✅ Workflow executed successfully!')
- console.log('Output:', result.output)
- if (result.metadata?.duration) {
- console.log('Duration:', result.metadata.duration, 'ms')
- }
- } else {
- console.log('❌ Workflow failed:', result.error)
- }
- } catch (error) {
- if (error instanceof SimStudioError) {
- console.error('SDK Error:', error.message, 'Code:', error.code)
- } else {
- console.error('Unexpected error:', error)
- }
- }
-}
-
-// Example 3: Workflow validation and status checking
-async function statusExample() {
- const client = new SimStudioClient({
- apiKey: process.env.SIM_API_KEY!,
- })
-
- try {
- // Check if workflow is ready
- const isReady = await client.validateWorkflow('your-workflow-id')
- console.log('Workflow ready:', isReady)
-
- // Get detailed status
- const status = await client.getWorkflowStatus('your-workflow-id')
- console.log('Status:', {
- deployed: status.isDeployed,
- needsRedeployment: status.needsRedeployment,
- deployedAt: status.deployedAt,
- })
-
- if (status.isDeployed) {
- // Execute the workflow
- const result = await client.executeWorkflow('your-workflow-id')
-
- if (result.success) {
- console.log('✅ Workflow executed successfully!')
- console.log('Output:', result.output)
- } else {
- console.log('❌ Workflow failed:', result.error)
- }
- }
- } catch (error) {
- if (error instanceof SimStudioError) {
- console.error('SDK Error:', error.message, 'Code:', error.code)
- } else {
- console.error('Unexpected error:', error)
- }
- }
-}
-
-// Example 4: Workflow execution with streaming
-async function streamingExample() {
- const client = new SimStudioClient({
- apiKey: process.env.SIM_API_KEY!,
- })
-
- try {
- const result = await client.executeWorkflow('your-workflow-id', {
- input: {
- message: 'Count to five',
- },
- stream: true,
- selectedOutputs: ['agent1.content'], // Use blockName.attribute format
- timeout: 60000,
- })
-
- if (result.success) {
- console.log('✅ Workflow executed successfully!')
- console.log('Output:', result.output)
- console.log('Duration:', result.metadata?.duration, 'ms')
- } else {
- console.log('❌ Workflow failed:', result.error)
- }
- } catch (error) {
- if (error instanceof SimStudioError) {
- console.error('SDK Error:', error.message, 'Code:', error.code)
- } else {
- console.error('Unexpected error:', error)
- }
- }
-}
-
-// Run examples
-if (require.main === module) {
- async function runExamples() {
- console.log('🚀 Running Sim SDK Examples\n')
-
- try {
- await basicExample()
- console.log('\n✅ Basic example completed')
-
- await withInputExample()
- console.log('\n✅ Input example completed')
-
- await statusExample()
- console.log('\n✅ Status example completed')
-
- await streamingExample()
- console.log('\n✅ Streaming example completed')
- } catch (error) {
- console.error('Error running examples:', error)
- }
- }
-
- runExamples()
-}
diff --git a/packages/ts-sdk/package.json b/packages/ts-sdk/package.json
deleted file mode 100644
index 01de89e72bc..00000000000
--- a/packages/ts-sdk/package.json
+++ /dev/null
@@ -1,59 +0,0 @@
-{
- "name": "simstudio-ts-sdk",
- "version": "0.2.1",
- "description": "Sim SDK - Execute workflows programmatically",
- "type": "module",
- "exports": {
- ".": {
- "types": "./dist/index.d.ts",
- "import": "./dist/index.js"
- }
- },
- "scripts": {
- "build": "tsc",
- "type-check": "tsc --noEmit",
- "lint": "biome check --write --unsafe .",
- "lint:check": "biome check .",
- "format": "biome format --write .",
- "format:check": "biome format .",
- "dev:watch": "tsc --watch",
- "prepublishOnly": "bun run build",
- "test": "vitest run",
- "test:watch": "vitest"
- },
- "files": [
- "dist"
- ],
- "keywords": [
- "simstudio",
- "ai",
- "workflow",
- "sdk",
- "api",
- "automation",
- "typescript"
- ],
- "author": "Sim",
- "license": "Apache-2.0",
- "devDependencies": {
- "@sim/tsconfig": "workspace:*",
- "@types/node": "24.19.2",
- "typescript": "^7.0.2",
- "vitest": "^5.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "publishConfig": {
- "access": "public"
- },
- "repository": {
- "type": "git",
- "url": "https://github.com/simstudioai/sim.git",
- "directory": "packages/ts-sdk"
- },
- "homepage": "https://sim.ai",
- "bugs": {
- "url": "https://github.com/simstudioai/sim/issues"
- }
-}
diff --git a/packages/ts-sdk/src/index.test.ts b/packages/ts-sdk/src/index.test.ts
deleted file mode 100644
index a979db84257..00000000000
--- a/packages/ts-sdk/src/index.test.ts
+++ /dev/null
@@ -1,260 +0,0 @@
-import { readFileSync } from 'node:fs'
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { SDK_VERSION, SimStudioClient } from './index'
-
-const mockFetch = vi.fn()
-
-beforeEach(() => {
- vi.stubGlobal('fetch', mockFetch)
-})
-
-function v2ExecutionResponse(output: unknown = {}, status = 'completed') {
- return {
- data: {
- runId: 'execution-123',
- workflowId: 'workflow-id',
- status,
- output,
- error: null,
- startedAt: '2026-08-11T12:00:00.000Z',
- endedAt: '2026-08-11T12:00:00.010Z',
- durationMs: 10,
- },
- }
-}
-
-describe('SimStudioClient', () => {
- let client: SimStudioClient
-
- beforeEach(() => {
- client = new SimStudioClient({
- apiKey: 'test-api-key',
- baseUrl: 'https://test.sim.ai',
- })
- vi.clearAllMocks()
- })
-
- describe('executeWorkflow - async execution', () => {
- it('throws when a sync workflow run completes with failed status', async () => {
- const failed = v2ExecutionResponse({ partial: true })
- failed.data.status = 'failed'
- failed.data.error = {
- code: 'BLOCK_EXECUTION_FAILED',
- message: 'Invalid credentials',
- }
- vi.mocked(mockFetch).mockResolvedValue({
- ok: true,
- status: 200,
- json: vi.fn().mockResolvedValue(failed),
- headers: { get: vi.fn().mockReturnValue(null) },
- })
-
- await expect(client.executeWorkflow('workflow-id', {})).rejects.toMatchObject({
- name: 'SimStudioError',
- code: 'BLOCK_EXECUTION_FAILED',
- message: 'Invalid credentials',
- })
- })
-
- it('reports a cancelled sync run as unsuccessful', async () => {
- vi.mocked(mockFetch).mockResolvedValue({
- ok: true,
- status: 200,
- json: vi.fn().mockResolvedValue(v2ExecutionResponse({}, 'cancelled')),
- headers: { get: vi.fn().mockReturnValue(null) },
- })
-
- const result = await client.executeWorkflow('workflow-id', {})
-
- expect(result).toHaveProperty('success', false)
- })
-
- it('rejects a server-side timeout for sync execution', async () => {
- await expect(
- client.executeWorkflow('workflow-id', {}, { executionTimeoutSeconds: 90 })
- ).rejects.toMatchObject({ code: 'INVALID_EXECUTION_TIMEOUT' })
- expect(mockFetch).not.toHaveBeenCalled()
- })
-
- it('rejects a server-side timeout above seven days', async () => {
- await expect(
- client.executeWorkflow(
- 'workflow-id',
- {},
- {
- async: true,
- executionTimeoutSeconds: 604_801,
- }
- )
- ).rejects.toMatchObject({ code: 'INVALID_EXECUTION_TIMEOUT' })
- expect(mockFetch).not.toHaveBeenCalled()
- })
- })
-
- describe('executeWithRetry', () => {
- it('should retry on rate limit error', async () => {
- // First call returns 429, second call succeeds
- const rateLimitResponse = {
- ok: false,
- status: 429,
- statusText: 'Too Many Requests',
- json: vi.fn().mockResolvedValue({
- error: 'Rate limit exceeded',
- code: 'RATE_LIMIT_EXCEEDED',
- }),
- headers: {
- get: vi.fn((header: string) => {
- if (header === 'retry-after') return '1'
- if (header === 'x-ratelimit-limit') return '100'
- if (header === 'x-ratelimit-remaining') return '0'
- if (header === 'x-ratelimit-reset') return String(Math.floor(Date.now() / 1000) + 60)
- return null
- }),
- },
- }
-
- const successResponse = {
- ok: true,
- status: 200,
- json: vi.fn().mockResolvedValue(v2ExecutionResponse({ result: 'success' })),
- headers: {
- get: vi.fn().mockReturnValue(null),
- },
- }
-
- vi.mocked(mockFetch)
- .mockResolvedValueOnce(rateLimitResponse as any)
- .mockResolvedValueOnce(successResponse as any)
-
- const result = await client.executeWithRetry(
- 'workflow-id',
- { message: 'test' },
- {},
- { maxRetries: 3, initialDelay: 10 }
- )
-
- expect(result).toHaveProperty('success', true)
- expect(vi.mocked(mockFetch)).toHaveBeenCalledTimes(2)
- })
-
- it('should throw after max retries exceeded', async () => {
- const mockResponse = {
- ok: false,
- status: 429,
- statusText: 'Too Many Requests',
- json: vi.fn().mockResolvedValue({
- error: 'Rate limit exceeded',
- code: 'RATE_LIMIT_EXCEEDED',
- }),
- headers: {
- get: vi.fn((header: string) => {
- if (header === 'retry-after') return '1'
- return null
- }),
- },
- }
-
- vi.mocked(mockFetch).mockResolvedValue(mockResponse as any)
-
- await expect(
- client.executeWithRetry(
- 'workflow-id',
- { message: 'test' },
- {},
- { maxRetries: 2, initialDelay: 10 }
- )
- ).rejects.toThrow('Rate limit exceeded')
-
- expect(vi.mocked(mockFetch)).toHaveBeenCalledTimes(3) // Initial + 2 retries
- })
-
- it('should not retry on non-rate-limit errors', async () => {
- const mockResponse = {
- ok: false,
- status: 500,
- statusText: 'Internal Server Error',
- json: vi.fn().mockResolvedValue({
- error: {
- code: 'INTERNAL_ERROR',
- message: 'Server error',
- },
- }),
- headers: {
- get: vi.fn().mockReturnValue(null),
- },
- }
-
- vi.mocked(mockFetch).mockResolvedValue(mockResponse as any)
-
- await expect(client.executeWithRetry('workflow-id', { message: 'test' })).rejects.toThrow(
- 'Server error'
- )
-
- expect(vi.mocked(mockFetch)).toHaveBeenCalledTimes(1) // No retries
- })
- })
-
- describe('getRateLimitInfo', () => {
- it('parses an ISO x-ratelimit-reset, the format the v2 API sends', async () => {
- const mockResponse = {
- ok: true,
- status: 200,
- json: vi.fn().mockResolvedValue(v2ExecutionResponse()),
- headers: {
- get: vi.fn((header: string) => {
- if (header === 'x-ratelimit-limit') return '100'
- if (header === 'x-ratelimit-remaining') return '99'
- if (header === 'x-ratelimit-reset') return '2024-01-01T00:00:00.000Z'
- return null
- }),
- },
- }
-
- vi.mocked(mockFetch).mockResolvedValue(mockResponse as any)
-
- await client.executeWorkflow('workflow-id', {})
-
- expect(client.getRateLimitInfo()?.reset).toBe(1704067200000)
- })
- })
-})
-
-describe('client identity', () => {
- it('keeps SDK_VERSION in step with package.json', () => {
- const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
- expect(SDK_VERSION).toBe(manifest.version)
- })
-
- it('identifies the SDK on every request', async () => {
- const client = new SimStudioClient({ apiKey: 'test-api-key', baseUrl: 'https://test.sim.ai' })
- vi.mocked(mockFetch).mockResolvedValue({
- ok: true,
- status: 200,
- headers: { get: () => null },
- json: async () => ({ data: { isDeployed: true } }),
- } as any)
-
- await client.getWorkflowStatus('workflow-id')
-
- const headers = vi.mocked(mockFetch).mock.calls[0][1]?.headers as Record
- expect(headers['X-Sim-Client-Info']).toBe(
- `sdk-js/${SDK_VERSION}; node/${process.versions.node}`
- )
- expect(headers['User-Agent']).toBe(
- `simstudio-ts-sdk/${SDK_VERSION} node/${process.versions.node}`
- )
- expect(headers['X-API-Key']).toBe('test-api-key')
- })
-
- it('sends requests to the canonical www host when no baseUrl is given', async () => {
- const client = new SimStudioClient({ apiKey: 'test-api-key' })
- vi.mocked(mockFetch).mockClear()
- vi.mocked(mockFetch).mockResolvedValue(
- new Response(JSON.stringify({ data: { isDeployed: true } }), { status: 200 })
- )
-
- await client.getWorkflowStatus('workflow-id')
-
- expect(String(vi.mocked(mockFetch).mock.calls[0][0])).toMatch(/^https:\/\/www\.sim\.ai\//)
- })
-})
diff --git a/packages/ts-sdk/src/index.ts b/packages/ts-sdk/src/index.ts
deleted file mode 100644
index 76f0a9834a4..00000000000
--- a/packages/ts-sdk/src/index.ts
+++ /dev/null
@@ -1,727 +0,0 @@
-export interface SimStudioConfig {
- apiKey: string
- baseUrl?: string
-}
-
-/**
- * The published package version. Kept in step with `package.json` by a test
- * rather than read at runtime, so the SDK stays usable where there is no file
- * system to read it from.
- */
-export const SDK_VERSION = '0.2.1'
-
-/**
- * Identifies this SDK to the API on every request, the way every official Sim
- * client does, so a server log line or analytics event can say which client
- * made the call. `X-Sim-Client-Info` is the header the server reads and works
- * everywhere; `User-Agent` is added only where the runtime lets a script set it.
- */
-const CLIENT_HEADERS: Readonly> = (() => {
- const node = typeof process !== 'undefined' ? process.versions?.node : undefined
- const runtime = node ? `; node/${node}` : ''
- const headers: Record = {
- 'X-Sim-Client-Info': `sdk-js/${SDK_VERSION}${runtime}`,
- }
- if (node) headers['User-Agent'] = `simstudio-ts-sdk/${SDK_VERSION} node/${node}`
- return headers
-})()
-
-export interface LargeValueRef {
- __simLargeValueRef: true
- version: 1
- id: string
- kind: 'array' | 'object' | 'string' | 'json'
- size: number
- /** Opaque execution-scoped server storage key. This is not a download URL. */
- key?: string
- executionId?: string
- preview?: unknown
-}
-
-export interface WorkflowExecutionResult {
- success: boolean
- executionId?: string
- output?: any
- error?: string
- logs?: any[]
- metadata?: {
- duration?: number
- executionId?: string
- runId?: string
- startTime?: string
- endTime?: string
- [key: string]: any
- }
- traceSpans?: any[]
- totalDuration?: number
-}
-
-export interface WorkflowStatus {
- isDeployed: boolean
- isPublished?: boolean
- deployedAt?: string
- needsRedeployment: boolean
-}
-
-export interface ExecutionOptions {
- /** Client-side HTTP timeout in milliseconds. */
- timeout?: number
- stream?: boolean
- selectedOutputs?: string[]
- async?: boolean
- /** Server-side async execution cap in seconds (1–604800). */
- executionTimeoutSeconds?: number
-}
-
-export type SyncExecutionOptions = Omit & {
- /** Async mode is controlled by executeWorkflowSync and cannot be overridden. */
- async?: never
- /** Server-side execution timeout overrides are async-only. */
- executionTimeoutSeconds?: never
-}
-
-const MAX_EXECUTION_TIMEOUT_SECONDS = 604_800
-
-export interface AsyncExecutionResult {
- success: boolean
- runId: string
- statusUrl: string
- message: string
- async: true
-}
-
-export interface JobStatusResult {
- taskId: string
- status: string
- metadata?: Record
- output?: unknown
- error?: string
-}
-
-export interface WorkflowExecutionError {
- code: string
- message: string
- details?: unknown
-}
-
-export interface WorkflowRunStatus {
- runId: string
- workflowId: string
- status: 'queued' | 'pending' | 'running' | 'paused' | 'completed' | 'failed' | 'cancelled'
- trigger: string | null
- startedAt: string | null
- endedAt: string | null
- durationMs: number | null
- paused: Record | null
- cost: { total: number } | null
- error: WorkflowExecutionError | null
- output: unknown | null
- blockOutputs: Record | null
-}
-
-export interface GetWorkflowRunOptions {
- includeOutput?: boolean
- selectedOutputs?: string[]
-}
-
-export interface RateLimitInfo {
- limit: number
- remaining: number
- reset: number
- retryAfter?: number
-}
-
-export interface RetryOptions {
- maxRetries?: number
- initialDelay?: number
- maxDelay?: number
- backoffMultiplier?: number
-}
-
-export interface UsageLimits {
- success: boolean
- rateLimit: {
- sync: {
- isLimited: boolean
- requestsPerMinute: number
- maxBurst: number
- remaining: number
- resetAt: string
- }
- async: {
- isLimited: boolean
- requestsPerMinute: number
- maxBurst: number
- remaining: number
- resetAt: string
- }
- authType: string
- }
- usage: {
- currentPeriodCost: number
- limit: number
- plan: string
- }
- storage: {
- usedBytes: number
- limitBytes: number
- percentUsed: number
- }
-}
-
-/**
- * Native fetch reports network failures as a bare `TypeError: fetch failed` and puts the
- * underlying reason (ECONNREFUSED, DNS, TLS) on `cause`. Fold it into the message so callers
- * keep the diagnostic detail, and return the plain message unchanged when there is no cause.
- */
-function describeError(error: any): string | undefined {
- const message: string | undefined = error?.message
- const cause: string | undefined = error?.cause?.message
- if (message && cause && !message.includes(cause)) {
- return `${message}: ${cause}`
- }
- return message
-}
-
-export class SimStudioError extends Error {
- public code?: string
- public status?: number
-
- constructor(message: string, code?: string, status?: number) {
- super(message)
- this.name = 'SimStudioError'
- this.code = code
- this.status = status
- }
-}
-
-/**
- * Remove trailing slashes from a URL
- * Uses string operations instead of regex to prevent ReDoS attacks
- * @param url - The URL to normalize
- * @returns URL without trailing slashes
- */
-function normalizeBaseUrl(url: string): string {
- let normalized = url
- while (normalized.endsWith('/')) {
- normalized = normalized.slice(0, -1)
- }
- return normalized
-}
-
-export class SimStudioClient {
- private apiKey: string
- private baseUrl: string
- private rateLimitInfo: RateLimitInfo | null = null
-
- constructor(config: SimStudioConfig) {
- this.apiKey = config.apiKey
- this.baseUrl = normalizeBaseUrl(config.baseUrl || 'https://www.sim.ai')
- }
-
- /** The headers every request carries: the credential and the client's identity. */
- private requestHeaders(): Record {
- return { ...CLIENT_HEADERS, 'X-API-Key': this.apiKey }
- }
-
- /**
- * Convert File objects in input to API format (base64)
- * Recursively processes nested objects and arrays
- */
- private async convertFilesToBase64(
- value: any,
- visited: WeakSet