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-sdkCreate 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.
| 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.
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 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
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) -> voidgetRateLimitInfo 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
| 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 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: numberrequestsPerMinute 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.