# TypeScript (/api-reference/typescript)





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 [#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<string>(['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.

<Callout type="warn">
  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).
</Callout>

## Method reference [#api-reference]

### Constructor [#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 [#execute]

```text
executeWorkflow(workflowId: string, input?: any, options?: ExecutionOptions)
  -> Promise<WorkflowExecutionResult | AsyncExecutionResult>
executeWorkflowSync(workflowId: string, input?: any, options?: SyncExecutionOptions)
  -> Promise<WorkflowExecutionResult>
executeWithRetry(workflowId: string, input?: any, options?: ExecutionOptions,
  retryOptions?: RetryOptions) -> Promise<WorkflowExecutionResult | AsyncExecutionResult>
```

| `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 [#read-deployment-and-runs]

```text
getWorkflowStatus(workflowId: string) -> Promise<WorkflowStatus>
validateWorkflow(workflowId: string) -> Promise<boolean>
getWorkflowRun(workflowId: string, runId: string, options?: GetWorkflowRunOptions)
  -> Promise<WorkflowRunStatus>
getJobStatus(taskId: string) -> Promise<JobStatusResult>
```

`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 [#read-limits-and-update-the-client]

```text
getRateLimitInfo() -> RateLimitInfo | null
getUsageLimits() -> Promise<UsageLimits>
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 [#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 [#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 [#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 [#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 [#jobstatusresult]

The legacy type has `taskId:string`, `status:string`, optional `metadata:Record<string,unknown>`, `output:unknown`, and `error:string`. It describes legacy job polling, not a v2 run receipt.

### LargeValueRef [#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 [#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 [#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 &#x2A;*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<string, unknown>) {
  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 [#nextjs-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 [#upgrade-from-01x]

`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 [#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)
