The official TypeScript/JavaScript SDK for SteelEngine provides full type safety and supports both Node.js and browser environments, allowing you to execute workflows programmatically from your Node.js applications, web applications, and other JavaScript environments.
The TypeScript SDK provides full type safety, async execution support, automatic rate limiting with exponential backoff, and usage tracking.
Installation
Install the SDK using your preferred package manager:
npm install @steelengine/sdkQuick Start
Here's a simple example to get you started:
import { SteelEngineClient } from '@steelengine/sdk';
// Initialize the client
const client = new SteelEngineClient({
apiKey: 'your-api-key-here',
baseUrl: 'https://steelengine.com' // optional, defaults to https://steelengine.com
});
// 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
SteelEngineClient
Constructor
new SteelEngineClient(config: SteelEngineConfig)Configuration:
config.apiKey(string): Your SteelEngine API keyconfig.baseUrl(string, optional): Base URL for the SteelEngine API (defaults to{{APP_URL}})
Methods
executeWorkflow()
Execute a workflow with optional input data.
const result = await client.executeWorkflow('workflow-id', { message: 'Hello, world!' }, {
timeout: 30000 // 30 seconds
});Parameters:
workflowId(string): The ID of the workflow to executeinput(any, optional): Input data to pass to the workflowoptions(ExecutionOptions, optional):timeout(number): Timeout in milliseconds (default: 30000)stream(boolean): Enable streaming responses (default: false)selectedOutputs(string[]): Block outputs to stream inblockName.attributeformat (e.g.,["agent1.content"])async(boolean): Execute asynchronously (default: false)
Returns: Promise<WorkflowExecutionResult | AsyncExecutionResult | PendingExecutionResult>
When async: true, returns HTTP 202 with a jobId, executionId, and statusUrl for polling. A synchronous request waits for completion, but may also return HTTP 202 with pending: true if its wait window expires. Poll that execution ID; another POST would start another run.
| Mode | Initial HTTP response | Where the result appears |
|---|---|---|
| Sync | The executed root Response block's status and raw JSON data, or an execution envelope | The SDK normalizes Response data into result.output |
| Async | HTTP 202 admission receipt | getJobStatus(jobId).output when status === 'completed' |
| Stream | HTTP 200 opens an SSE connection | onChunk receives selected outputs; the final message reports execution status and timing |
| Sync still running | HTTP 202 with pending: true | getJobStatus(executionId) retrieves the same run |
A Response block's authored status applies to sync HTTP delivery. In async output it is represented as { data, status, headers }; job lookup still returns HTTP 200. Streaming also keeps HTTP 200 and reports the execution outcome in its terminal message. A child workflow's Response does not override the parent HTTP response.
executeWorkflowResponse()
Use this method to inspect the response kind, raw Response body, status, and headers without normalization:
const response = await client.executeWorkflowResponse<{ answer: number }>('workflow-id');
if (response.kind === 'response') {
console.log(response.status, response.headers, response.body);
} else if (response.kind === 'async' || response.kind === 'pending') {
console.log(response.body.executionId, response.body.statusUrl);
} else {
console.log(response.body);
}Kinds come from the server's X-SteelEngine-Response-Type header, so authored fields such as success, async, or output are never mistaken for transport control fields. Bodyless Response statuses (204/205) return null. Non-2xx responses, including authored Response errors, throw SteelEngineError with status and body; they do not by themselves prove an engine failure.
HTTP 503 with code: 'RESULT_UNAVAILABLE' means the run's exact result could not be recovered. STREAM_INCOMPLETE means selected delivery could not be verified. Use the error's executionId or statusUrl to retrieve the existing run. An SDK timeout, abort, or disconnected stream cancels the HTTP request only. API-key callers can poll with the same authorized key; the builder's reconnect endpoint requires a session.
getWorkflowStatus()
Get the status of a workflow (deployment status, etc.).
const status = await client.getWorkflowStatus('workflow-id');
console.log('Is deployed:', status.isDeployed);Parameters:
workflowId(string): The ID of the workflow
Returns: Promise<WorkflowStatus>
validateWorkflow()
Validate that a workflow is ready for execution.
const isReady = await client.validateWorkflow('workflow-id');
if (isReady) {
// Workflow is deployed and ready
}Parameters:
workflowId(string): The ID of the workflow
Returns: Promise<boolean>
getJobStatus()
Get the status of an async job execution.
const status = await client.getJobStatus('job-id-from-async-execution');
console.log('Status:', status.status); // 'queued', 'processing', 'completed', 'failed'
if (status.status === 'completed') {
console.log('Output:', status.output);
}Parameters:
jobId(string): The job ID returned from async execution (AsyncExecutionResult.jobId)
Returns: Promise<JobStatus>
Response fields:
success(boolean): Whether the request was successfuljobId(string): The job IDstatus(string): One of'queued','processing','paused','completed','failed','cancelled'. Paused runs need input and are not complete.metadata(object): ContainsstartedAt,completedAt, anddurationoutput(any, optional): The workflow output (when completed)error(any, optional): Error details (when failed)estimatedDuration(number, optional): Estimated duration in milliseconds (when processing/queued)
executeWithRetry()
Execute a workflow with automatic retry on rate limit errors using exponential backoff.
const result = await client.executeWithRetry('workflow-id', { message: 'Hello' }, {
timeout: 30000
}, {
maxRetries: 3, // Maximum number of retries
initialDelay: 1000, // Initial delay in ms (1 second)
maxDelay: 30000, // Maximum delay in ms (30 seconds)
backoffMultiplier: 2 // Exponential backoff multiplier
});Parameters:
workflowId(string): The ID of the workflow to executeinput(any, optional): Input data to pass to the workflowoptions(ExecutionOptions, optional): Same asexecuteWorkflow()retryOptions(RetryOptions, optional):maxRetries(number): Maximum number of retries (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<WorkflowExecutionResult | AsyncExecutionResult>
The retry logic uses exponential backoff (1s → 2s → 4s → 8s...) with ±25% jitter to prevent thundering herd. If the API provides a retry-after header, it will be used instead.
getRateLimitInfo()
Get the current rate limit information from the last API response.
const rateLimitInfo = client.getRateLimitInfo();
if (rateLimitInfo) {
console.log('Limit:', rateLimitInfo.limit);
console.log('Remaining:', rateLimitInfo.remaining);
console.log('Reset:', new Date(rateLimitInfo.reset * 1000));
}Returns: RateLimitInfo | null
getUsageLimits()
Get current usage limits and quota information for your account.
const limits = await client.getUsageLimits();
console.log('Sync requests remaining:', limits.rateLimit.sync.remaining);
console.log('Async requests remaining:', limits.rateLimit.async.remaining);
console.log('Current period cost:', limits.usage.currentPeriodCost);
console.log('Plan:', limits.usage.plan);Returns: Promise<UsageLimits>
Response structure:
{
success: boolean
rateLimit: {
sync: {
isLimited: boolean
limit: number
remaining: number
resetAt: string
}
async: {
isLimited: boolean
limit: number
remaining: number
resetAt: string
}
authType: string // 'api' or 'manual'
}
usage: {
currentPeriodCost: number
limit: number
plan: string // e.g., 'free', 'pro'
}
}setApiKey()
Update the API key.
client.setApiKey('new-api-key');setBaseUrl()
Update the base URL.
client.setBaseUrl('https://my-custom-domain.com');Types
WorkflowExecutionResult
interface WorkflowExecutionResult {
success: boolean;
output?: any;
error?: string;
logs?: any[];
metadata?: {
duration?: number;
executionId?: string;
[key: string]: any;
};
traceSpans?: any[];
totalDuration?: number;
}AsyncExecutionResult
interface AsyncExecutionResult {
success: boolean;
async: true;
jobId: string;
executionId: string;
message: string;
statusUrl: string; // e.g., "https://steelengine.com/api/jobs/{jobId}"
}WorkflowStatus
interface WorkflowStatus {
isDeployed: boolean;
deployedAt?: string;
needsRedeployment: boolean;
}RateLimitInfo
interface RateLimitInfo {
limit: number;
remaining: number;
reset: number;
retryAfter?: number;
}UsageLimits
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;
};
}SteelEngineError
class SteelEngineError extends Error {
code?: string;
status?: number;
}Common error codes:
UNAUTHORIZED: Invalid API keyTIMEOUT: Request timed outRATE_LIMIT_EXCEEDED: Rate limit exceededUSAGE_LIMIT_EXCEEDED: Usage limit exceededEXECUTION_ERROR: Workflow execution failed
Examples
Basic Workflow Execution
Set up the SteelEngineClient with your API key.
Check if the workflow is deployed and ready for execution.
Run the workflow with your input data.
Process the execution result and handle any errors.
import { SteelEngineClient } from '@steelengine/sdk';
const client = new SteelEngineClient({
apiKey: process.env.STEELENGINE_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
Handle different types of errors that may occur during workflow execution:
import { SteelEngineClient, SteelEngineError } from '@steelengine/sdk';
const client = new SteelEngineClient({
apiKey: process.env.STEELENGINE_API_KEY!
});
async function executeWithErrorHandling() {
try {
const result = await client.executeWorkflow('workflow-id');
return result;
} catch (error) {
if (error instanceof SteelEngineError) {
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
Configure the client using environment variables:
import { SteelEngineClient } from '@steelengine/sdk';
// Development configuration
const apiKey = process.env.STEELENGINE_API_KEY;
if (!apiKey) {
throw new Error('STEELENGINE_API_KEY environment variable is required');
}
const client = new SteelEngineClient({
apiKey,
baseUrl: process.env.STEELENGINE_BASE_URL // optional
});Node.js Express Integration
Integrate with an Express.js server:
import express from 'express';
import { SteelEngineClient } from '@steelengine/sdk';
const app = express();
const client = new SteelEngineClient({
apiKey: process.env.STEELENGINE_API_KEY!
});
app.use(express.json());
app.post('/execute-workflow', async (req, res) => {
try {
const { workflowId, input } = req.body;
const result = await client.executeWorkflow(workflowId, input, {
timeout: 60000
});
res.json({
success: true,
data: result
});
} catch (error) {
console.error('Workflow execution error:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});Next.js API Route
Use with Next.js API routes:
// pages/api/workflow.ts or app/api/workflow/route.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { SteelEngineClient } from '@steelengine/sdk';
const client = new SteelEngineClient({
apiKey: process.env.STEELENGINE_API_KEY!
});
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { workflowId, input } = req.body;
const result = await client.executeWorkflow(workflowId, input, {
timeout: 30000
});
res.status(200).json(result);
} catch (error) {
console.error('Error executing workflow:', error);
res.status(500).json({
error: 'Failed to execute workflow'
});
}
}Browser Usage
Use in the browser (with proper CORS configuration):
import { SteelEngineClient } from '@steelengine/sdk';
// Note: In production, use a proxy server to avoid exposing API keys
const client = new SteelEngineClient({
apiKey: 'your-public-api-key', // Use with caution in browser
baseUrl: 'https://steelengine.com'
});
async function executeClientSideWorkflow() {
try {
const result = await client.executeWorkflow('workflow-id', {
userInput: 'Hello from browser'
});
console.log('Workflow result:', result);
// Update UI with result
document.getElementById('result')!.textContent =
JSON.stringify(result.output, null, 2);
} catch (error) {
console.error('Error:', error);
}
}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:
{
type: 'file',
data: 'data:mime/type;base64,base64data',
name: 'filename',
mime: 'mime/type'
}Alternatively, you can manually provide files using the URL format:
{
type: 'url',
data: 'https://example.com/file.pdf',
name: 'file.pdf',
mime: 'application/pdf'
}import { SteelEngineClient } from '@steelengine/sdk';
const client = new SteelEngineClient({
apiKey: process.env.NEXT_PUBLIC_STEELENGINE_API_KEY!
});
// From file input
async function handleFileUpload(event: Event) {
const input = event.target as HTMLInputElement;
const files = Array.from(input.files || []);
// Include files under the field name from your API trigger's input format
const result = await client.executeWorkflow('workflow-id', {
documents: files, // Must match your workflow's "files" field name
instructions: 'Analyze these documents'
});
console.log('Result:', result);
}When using the SDK in the browser, be careful not to expose sensitive API keys. Consider using a backend proxy or public API keys with limited permissions.
React Hook Example
Create a custom React hook for workflow execution:
import { useState, useCallback } from 'react';
import { SteelEngineClient, WorkflowExecutionResult } from '@steelengine/sdk';
const client = new SteelEngineClient({
apiKey: process.env.STEELENGINE_API_KEY!
});
interface UseWorkflowResult {
result: WorkflowExecutionResult | null;
loading: boolean;
error: Error | null;
executeWorkflow: (workflowId: string, input?: any) => Promise<void>;
}
export function useWorkflow(): UseWorkflowResult {
const [result, setResult] = useState<WorkflowExecutionResult | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const executeWorkflow = useCallback(async (workflowId: string, input?: any) => {
setLoading(true);
setError(null);
setResult(null);
try {
const workflowResult = await client.executeWorkflow(workflowId, input, {
timeout: 30000
});
setResult(workflowResult);
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error'));
} finally {
setLoading(false);
}
}, []);
return {
result,
loading,
error,
executeWorkflow
};
}
// Usage in component
function WorkflowComponent() {
const { result, loading, error, executeWorkflow } = useWorkflow();
const handleExecute = () => {
executeWorkflow('my-workflow-id', {
message: 'Hello from React!'
});
};
return (
<div>
<button onClick={handleExecute} disabled={loading}>
{loading ? 'Executing...' : 'Execute Workflow'}
</button>
{error && <div>Error: {error.message}</div>}
{result && (
<div>
<h3>Result:</h3>
<pre>{JSON.stringify(result, null, 2)}</pre>
</div>
)}
</div>
);
}Async Workflow Execution
Execute workflows asynchronously for long-running tasks:
import { SteelEngineClient, AsyncExecutionResult } from '@steelengine/sdk';
const client = new SteelEngineClient({
apiKey: process.env.STEELENGINE_API_KEY!
});
async function executeAsync() {
try {
// Start async execution
const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, {
async: true // Execute asynchronously
});
// Check if result is an async execution
if ('jobId' in result) {
console.log('Job ID:', result.jobId);
console.log('Status endpoint:', result.links.status);
// Poll for completion
let status = await client.getJobStatus(result.jobId);
while (status.status === 'queued' || status.status === 'processing') {
console.log('Current status:', status.status);
await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds
status = await client.getJobStatus(result.jobId);
}
if (status.status === 'completed') {
console.log('Workflow completed!');
console.log('Output:', status.output);
console.log('Duration:', status.metadata.duration);
} else {
console.error('Workflow failed:', status.error);
}
}
} catch (error) {
console.error('Error:', error);
}
}
executeAsync();Rate Limiting and Retry
Handle rate limits automatically with exponential backoff:
import { SteelEngineClient, SteelEngineError } from '@steelengine/sdk';
const client = new SteelEngineClient({
apiKey: process.env.STEELENGINE_API_KEY!
});
async function executeWithRetryHandling() {
try {
// Automatically retries on rate limit
const result = await client.executeWithRetry('workflow-id', { message: 'Process this' }, {}, {
maxRetries: 5,
initialDelay: 1000,
maxDelay: 60000,
backoffMultiplier: 2
});
console.log('Success:', result);
} catch (error) {
if (error instanceof SteelEngineError && error.code === 'RATE_LIMIT_EXCEEDED') {
console.error('Rate limit exceeded after all retries');
// Check rate limit info
const rateLimitInfo = client.getRateLimitInfo();
if (rateLimitInfo) {
console.log('Rate limit resets at:', new Date(rateLimitInfo.reset * 1000));
}
}
}
}Usage Monitoring
Monitor your account usage and limits:
import { SteelEngineClient } from '@steelengine/sdk';
const client = new SteelEngineClient({
apiKey: process.env.STEELENGINE_API_KEY!
});
async function checkUsage() {
try {
const limits = await client.getUsageLimits();
console.log('=== Rate Limits ===');
console.log('Sync requests:');
console.log(' Limit:', limits.rateLimit.sync.limit);
console.log(' Remaining:', limits.rateLimit.sync.remaining);
console.log(' Resets at:', limits.rateLimit.sync.resetAt);
console.log(' Is limited:', limits.rateLimit.sync.isLimited);
console.log('\nAsync requests:');
console.log(' Limit:', limits.rateLimit.async.limit);
console.log(' Remaining:', limits.rateLimit.async.remaining);
console.log(' Resets at:', limits.rateLimit.async.resetAt);
console.log(' Is limited:', limits.rateLimit.async.isLimited);
console.log('\n=== Usage ===');
console.log('Current period cost: $' + limits.usage.currentPeriodCost.toFixed(2));
console.log('Limit: $' + limits.usage.limit.toFixed(2));
console.log('Plan:', limits.usage.plan);
const percentUsed = (limits.usage.currentPeriodCost / limits.usage.limit) * 100;
console.log('Usage: ' + percentUsed.toFixed(1) + '%');
if (percentUsed > 80) {
console.warn('⚠️ Warning: You are approaching your usage limit!');
}
} catch (error) {
console.error('Error checking usage:', error);
}
}
checkUsage();Streaming Workflow Execution
Execute workflows with real-time streaming responses:
import { SteelEngineClient } from '@steelengine/sdk';
const client = new SteelEngineClient({
apiKey: process.env.STEELENGINE_API_KEY!
});
async function executeWithStreaming() {
try {
// Enable streaming for specific block outputs
const result = await client.executeWorkflow('workflow-id', { message: 'Count to five' }, {
stream: true,
selectedOutputs: ['agent1.content'], // Use blockName.attribute format
onChunk: ({ blockId, outputId, invocationId, chunk }) => {
// Accumulate each field/invocation separately when selecting several outputs.
process.stdout.write(chunk);
}
});
console.log('Workflow result:', result);
} catch (error) {
console.error('Error:', error);
}
}The SDK requests stream version 2 with X-SteelEngine-Stream-Version: 2. Direct HTTP clients should send that header too; callers without it retain the legacy nested final message. The streaming response follows the Server-Sent Events (SSE) format:
data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":"One"}
data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":", two"}
data: {"event":"done","success":true,"output":{},"metadata":{"duration":610}}
data: [DONE]Chunks also carry outputId and invocationId, distinguishing multiple fields and repeated blocks. A single selected content field can arrive as live text when its block supports it; other selected values arrive after their producing block settles. Strings are unchanged; other values are JSON-serialized. No separators are inserted. A successful selected stream ends with done.output: {} because the values were already delivered. Without selections, done.output contains the terminal workflow output.
The SDK resolves only after a terminal result and [DONE]. Received chunks or a closed connection alone are not success. A terminal status: 'paused' ends this stream segment while the run waits for input. Check success and status before treating the workflow as finished.
Timing values are milliseconds. When timingSemanticsVersion is 2, duration reflects active execution time, while waitDurationMs and elapsedDurationMs distinguish waiting from total wall time. Repeated polling retains the original completion timestamp and timing.
Using streaming in a web app:
Run the SDK on your backend and forward its onChunk messages through your application's authenticated streaming endpoint. In the UI, keep each selected field/invocation separate, show incoming chunks as progress, and clear the loading state only after the final result. Show a paused run as waiting for input, and a disconnected stream as incomplete with a link to check its execution status.
If you consume raw SSE instead, retain the decoder and an unfinished-frame buffer across network reads. A network read can end halfway through a UTF-8 character or a JSON message. Require the terminal result and [DONE]; do not ignore malformed JSON or treat transport closure as successful completion.
Getting Your API Key
Navigate to SteelEngine and log in to your account.
Navigate to the workflow you want to execute programmatically.
Click on "Deploy" to deploy your workflow if it hasn't been deployed yet.
During the deployment process, select or create an API key.
Copy the API key to use in your TypeScript/JavaScript application.
Requirements
- Node.js 16+
- TypeScript 5.0+ (for TypeScript projects)