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

The ESM package requires Node.js 18+:

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 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

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

The receipt's success:true confirms acceptance, not completion. Poll the returned run ID:

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 and Resume workflow run.

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

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.

CodeMeaning
UNAUTHORIZEDMissing, invalid, or expired credential
BAD_REQUESTV2 request validation failed
TIMEOUTThe client stopped waiting
RATE_LIMIT_EXCEEDEDSDK wrapper for HTTP 429
USAGE_LIMIT_EXCEEDEDA usage policy rejected the request or run
INVALID_EXECUTION_TIMEOUTAn unsupported or invalid async timeout option
EXECUTION_ERRORTransport, response parsing, or execution-request failure
EXECUTION_FAILEDFallback code for a failed synchronous run
STATUS_ERROR, USAGE_ERRORA 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.

executeWithRetry retries only rate-limit exceptions. By default it makes an initial attempt and up to three retries, using exponential backoff and ±25% jitter:

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.

Method reference

Constructor

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

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 fieldBehavior and default
timeout?:numberClient wait in milliseconds; default 30000
async?:booleanDefault omitted/false; true queues the active deployment
executionTimeoutSeconds?:numberAsync-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?:booleanDefault 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

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:

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

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

The SDK supplies declarations for these types. Arbitrary workflow input/output and log fields are not schema-typed by the client.

WorkflowExecutionResult

FieldDeclared typeCurrent adapter behavior
successbooleanTrue for completed/paused; false for cancelled; failed rejects
executionIdOptional stringV2 run ID
outputOptional anyWorkflow output
errorOptional stringReturned message when present
metadataOptional objectduration, executionId, runId, startTime, endTime; permits other keys
totalDurationOptional numberDuration in milliseconds
logs, traceSpansOptional 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:

FieldShape
runId, workflowIdStrings
statusqueued, pending, running, redacting, paused, completed, failed, or cancelled
triggerString or null
startedAt, endedAtISO timestamp or null; queued startedAt is enqueue time
durationMsNumber or null
pausedPause-detail object or null; inspect pauseKind and resumeAt
cost{total:number} in credits, or null when unavailable/withheld
errorWorkflowExecutionError with code/message and optional details, or null
outputRequested completed-run output, otherwise null
blockOutputsSelected block-ID/path values, or null
filesProduced-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 for the full contract, functional-output permission conflict, and file limits.

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

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.

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:

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

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.

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 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

The SDK recursively converts global File objects to:

const inlineFile = {
  type: 'file',
  data: 'data:application/pdf;base64,BASE64_BYTES',
  name: 'document.pdf',
  mime: 'application/pdf',
}

You can also supply a URL file:

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:

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

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:

async function executeAuthorizedWorkflow(workflowId: string, input: Record<string, unknown>) {
  return client.executeWorkflowSync(workflowId, input, { timeout: 60000 })
}

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

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:

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 and the 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.