Comprehensive reference for developing, extending, and debugging the Conductor JavaScript SDK.
Public API (src/sdk/index.ts)
|
+-- OrkesClients Factory for all domain clients
+-- Builders Task builders + ConductorWorkflow DSL
+-- Worker Framework @worker, TaskHandler, TaskContext, Metrics
|
+-- Domain Clients (src/sdk/clients/)
| WorkflowExecutor, TaskClient, MetadataClient, SchedulerClient,
| AuthorizationClient, SecretClient, SchemaClient, IntegrationClient,
| PromptClient, ApplicationClient, EventClient, HumanExecutor,
| TemplateClient, ServiceRegistryClient
|
+-- OpenAPI Layer (src/open-api/)
| Auto-generated types + resource classes
| Extended types in src/open-api/types.ts
|
+-- HTTP Transport (src/sdk/createConductorClient/)
Auth token lifecycle, retry with backoff, HTTP/2, TLS, proxy
Everything flows through src/sdk/index.ts:
sdk/index.ts
-> createConductorClient/index.ts (createConductorClient, orkesConductorClient alias, OrkesApiConfig)
-> OrkesClients.ts (OrkesClients factory)
-> clients/index.ts (14 client classes, all re-exported)
-> builders/index.ts (task builders, ConductorWorkflow, workflow(), taskDefinition())
-> generators/index.ts (legacy generators, still exported)
-> types.ts (OrkesApiConfig, TaskResultStatus)
-> worker/index.ts (TaskHandler, worker, getTaskContext, MetricsCollector, etc.)
-> helpers/logger.ts (DefaultLogger, noopLogger, ConductorLogger type)
-> helpers/errors.ts (ConductorSdkError type)
When adding new exports, follow this chain. If something isn't importable from "../sdk" in tests, it's missing from an index.ts somewhere in the chain.
src/
sdk/
index.ts # Public API exports
OrkesClients.ts # Factory: one Client -> 14 domain client getters
types.ts # OrkesApiConfig, TaskResultStatus, TaskResultOutputData
helpers/
errors.ts # ConductorSdkError, handleSdkError (throw/log strategies)
logger.ts # ConductorLogger interface, DefaultLogger, noopLogger
createConductorClient/ # Client construction
createConductorClient.ts # Main factory: config -> authenticated Client
constants.ts # Default timeouts, refresh intervals
helpers/
handleAuth.ts # Token gen, background refresh, mutex for concurrency
fetchWithRetry.ts # Transport + 429 rate-limit + 401/403 auth retries
resolveOrkesConfig.ts # Env vars > config object > defaults
resolveFetchFn.ts # Picks fetch impl (native, Undici)
getUndiciHttp2FetchFn.ts # HTTP/2 with connection pooling via Undici
addResourcesBackwardCompatibility.ts # Legacy method aliases
clients/ # Domain-specific API clients
workflow/
WorkflowExecutor.ts # 28 methods: register, start, execute, signal, etc.
helpers/ # enhanceSignalResponse, reverseFind, isCompletedTaskMatchingType
types.ts # EnhancedSignalResponse, TaskFinderPredicate
constants.ts # RETRY_TIME_IN_MILLISECONDS
task/TaskClient.ts # 8 methods: search, get, update, logs, queue
metadata/MetadataClient.ts # 21 methods: tasks, workflows, tags, rate limits
scheduler/SchedulerClient.ts # 14 methods: schedules, tags, global ops
authorization/AuthorizationClient.ts # 19 methods: users, groups, permissions
secret/SecretClient.ts # 9 methods: CRUD, tags, existence
schema/SchemaClient.ts # 6 methods: register, get, delete, versioning
integration/IntegrationClient.ts # 20 methods: providers, APIs, tags, prompts
prompt/PromptClient.ts # 9 methods: CRUD, tags, test against LLM
application/ApplicationClient.ts
event/EventClient.ts
human/HumanExecutor.ts
template/TemplateClient.ts
service-registry/ServiceRegistryClient.ts
worker/ # Polling & execution engine (internal)
TaskRunner.ts # Per-worker: poll -> execute -> update loop
Poller.ts # Generic concurrent queue with adaptive backoff
events/ # EventDispatcher + TaskRunnerEventsListener interface
exceptions/Exceptions.ts # NonRetryableException class
types.ts # ConductorWorker, TaskInProgressResult
constants.ts # Default poll intervals, batch sizes
builders/
ConductorWorkflow.ts # Fluent builder: add, fork, register, execute
workflow.ts # workflow(name, tasks) -> minimal WorkflowDef
taskDefinition.ts # taskDefinition({ name, ... }) -> TaskDef with defaults
tasks/ # 21 standard task builders
simple.ts # simpleTask(refName, taskDefName, input, optional?)
http.ts # httpTask(refName, httpInput, asyncComplete?, optional?)
wait.ts # waitTaskDuration(refName, duration), waitTaskUntil(refName, until)
subWorkflow.ts, setVariable.ts, inline.ts, dynamic.ts, dynamicFork.ts
event.ts, forkJoin.ts, join.ts, jsonJq.ts, kafkaPublish.ts
switch.ts, terminate.ts, humanTask.ts, startWorkflow.ts
getDocument.ts, httpPoll.ts, waitForWebhook.ts
llm/ # 13 LLM-specific builders
llmChatComplete.ts # llmChatCompleteTask(ref, provider, model, options)
llmTextComplete.ts, llmGenerateEmbeddings.ts, llmIndexDocument.ts
llmIndexText.ts, llmSearchIndex.ts, llmSearchEmbeddings.ts
llmStoreEmbeddings.ts, llmQueryEmbeddings.ts
generateImage.ts, generateAudio.ts, callMcpTool.ts, listMcpTools.ts
promptHelpers.ts # Prompt template utilities
types.ts # Role enum, LLMProvider enum, ChatMessage, ToolSpec, etc.
worker/
index.ts # Re-exports everything below
core/TaskHandler.ts # Main entry: discovers @worker fns, manages lifecycle
decorators/
worker.ts # @worker decorator (dual-mode: execution + builder)
registry.ts # Global registry: registerWorker, getRegisteredWorkers, clearWorkerRegistry
context/
TaskContext.ts # AsyncLocalStorage-based per-task context + getTaskContext()
metrics/
MetricsCollector.ts # TaskRunnerEventsListener impl, 19 metric types, quantiles
MetricsServer.ts # HTTP server: /metrics (Prometheus) + /health (JSON)
PrometheusRegistry.ts # Optional prom-client bridge (lazy loaded)
schema/ # jsonSchema(), schemaField() decorator, generateSchemaFromClass()
config/ # Worker configuration resolution helpers
generators/ # Legacy task generators (pre-v3, still exported for backward compat)
open-api/
index.ts # Type re-exports (generated + custom)
types.ts # Extended types: Consistency, ReturnStrategy, TaskType (28 variants),
# TaskResultStatusEnum, ServiceType, specific TaskDef types,
# Extended* interfaces for fields missing from OpenAPI spec
generated/ # AUTO-GENERATED from OpenAPI spec - never edit directly
types.gen.ts # All OpenAPI types
sdk.gen.ts # Resource classes (API call implementations)
client.ts # createClient factory
integration-tests/ # E2E tests against real Conductor server
utils/
waitForWorkflowStatus.ts # Poll until workflow reaches expected status
waitForWorkflowCompletion.ts
executeWorkflowWithRetry.ts # Retry on transient network errors (fetch failed, timeout, etc.)
customJestDescribe.ts # describeForOrkesV5 (skips if ORKES_BACKEND_VERSION < 5)
mockLogger.ts # jest.fn() logger for unit tests
Single authenticated connection, multiple domain facades:
const clients = await OrkesClients.from({
serverUrl: "https://play.orkes.io",
keyId: "...",
keySecret: "...",
});
const workflow = clients.getWorkflowClient(); // WorkflowExecutor
const metadata = clients.getMetadataClient(); // MetadataClient
// 12 more getters...OrkesClients.from() calls createConductorClient() which sets up auth, retry, and optional HTTP/2. You can also construct clients directly: new MetadataClient(client).
orkesConductorClient is an alias for createConductorClient. Integration tests use orkesConductorClient() by convention (reads env vars).
Every client method:
export class SomeClient {
public readonly _client: Client;
constructor(client: Client) { this._client = client; }
public async someMethod(arg1: string): Promise<SomeType> {
try {
const { data } = await SomeResource.apiCall({
path: { id: arg1 },
query: { ... },
body: ...,
client: this._client,
throwOnError: true,
});
return data;
} catch (error: unknown) {
handleSdkError(error, `Failed to do X for '${arg1}'`);
}
}
}Rules:
throwOnError: trueon every OpenAPI call- try/catch with
handleSdkErrorusing human-readable context with the resource ID - Return
datafrom the response - For APIs not in the OpenAPI spec, use
this._client.put/get/deletedirectly (see rate limit methods in MetadataClient)
handleSdkError(error, "Context"); // throws ConductorSdkError (return type: never)
handleSdkError(error, "Context", "log"); // logs to console.error (return type: void)The "throw" overload returns never, which is how client methods compile without a return after the catch. If TypeScript complains about a missing return in a new client method, you forgot the handleSdkError call.
The error message chains: "Failed to get workflow 'abc': 404 Not Found".
const wf = new ConductorWorkflow(executor, "order_flow")
.add(simpleTask("validate_ref", "validate_order", {}))
.fork([
[simpleTask("email_ref", "send_email", {})],
[simpleTask("sms_ref", "send_sms", {})],
])
.timeoutSeconds(3600)
.outputParameters({ orderId: "${workflow.input.orderId}" });
await wf.register(); // overwrite=true by default
const run = await wf.execute({ orderId: "123" });Gotcha: ConductorWorkflow.register() defaults to overwrite=true, but MetadataClient.registerWorkflowDef() defaults to overwrite=false.
execute() internally generates a requestId via crypto.randomUUID().
input("fieldName") returns "${workflow.input.fieldName}" and output("fieldName") returns "${workflow.output.fieldName}" - these are Conductor expression helpers.
@worker({ taskDefName: "process_order", concurrency: 5, pollInterval: 100 })
async function processOrder(task: Task) {
const ctx = getTaskContext();
ctx?.addLog("Processing started");
return { status: "COMPLETED", outputData: { result: "done" } };
}
const handler = new TaskHandler({ client, scanForDecorated: true });
await handler.startWorkers();
// handler.stopWorkers() to shut downLifecycle: TaskHandler discovers decorated workers from global registry -> creates TaskRunner per worker -> Poller polls for tasks -> executes with TaskContext -> updates result -> dispatches events.
NonRetryableException: throw from a worker to mark the task as FAILED_WITH_TERMINAL_ERROR (no retries regardless of task def retry settings).
throw new NonRetryableException("Order not found - permanent failure");function deepHelper() {
const ctx = getTaskContext(); // Works from any async depth
ctx?.addLog("Inside helper");
ctx?.setCallbackAfter(30);
ctx?.setOutput({ partial: true });
return ctx?.getInput();
}Returns undefined outside task execution. All 16 methods: getTaskId(), getWorkflowInstanceId(), getRetryCount(), getPollCount(), getInput(), getTaskDefName(), getWorkflowTaskType(), getTask(), addLog(), getLogs(), setCallbackAfter(), getCallbackAfterSeconds(), setOutput(), getOutput().
const metrics = new MetricsCollector({ prefix: "my_app", slidingWindowSize: 1000 });
const handler = new TaskHandler({ client, eventListeners: [metrics], scanForDecorated: true });
await handler.startWorkers();
// Programmatic access
const m = metrics.getMetrics();
console.log(m.pollTotal.get("my_task"));
// Prometheus text
const text = metrics.toPrometheusText();For an HTTP endpoint, use MetricsServer directly:
const server = new MetricsServer(metrics, 9090);
await server.start();
// GET http://localhost:9090/metrics -> Prometheus text
// GET http://localhost:9090/health -> {"status":"UP"}
await server.stop();Do not use MetricsCollector({ httpPort }) in Jest - it uses a dynamic import with .js extension that doesn't resolve under Jest's TS transform.
Every item below caused real test failures during SDK development.
The #1 source of bugs. simpleTask(taskReferenceName, name, inputParameters) puts the ref name first:
simpleTask("my_ref", "my_task_def", { key: "val" }) // CORRECT
simpleTask("my_task_def", "my_ref", { key: "val" }) // WRONG - silent failureGetting it backwards doesn't throw - it creates a workflow with swapped values. You only find out when the server says "taskReferenceName should be unique" (if you use the same task def name for two tasks).
All builders follow the same convention: (taskReferenceName, ...).
Also note: the export is setVariableTask (not setVariable), waitTaskDuration (not waitTask).
| Type | Spec Says | Reality | Fix |
|---|---|---|---|
TargetRef.id |
Literal string type | Dynamic string | as never |
UpsertUserRequest.roles |
Single string union | People expect array | "USER" not ["USER"] |
UpsertGroupRequest.roles |
Single string union | People expect array | "USER" not ["USER"] |
TaskResult |
taskId, workflowInstanceId required |
Often constructed partial | Always provide both |
TaskMock |
{ status?, output?, executionTime?, queueWaitTime? } |
People write { COMPLETED: {...} } |
Use array: [{ status: "COMPLETED", output: {...} }] |
RateLimitConfig |
Only concurrentExecLimit + rateLimitKey |
Server accepts more fields | Use ExtendedRateLimitConfig or raw HTTP |
Task.taskType |
Expect "SIMPLE" for simple tasks |
Actually the task def name | Don't compare against TaskType enum |
SchemaDef.type |
"JSON" | "AVRO" | "PROTOBUF" |
People pass "json" |
Case-sensitive string literal |
ChatMessage.role |
Role enum |
People pass "user" |
Use Role.USER from sdk/builders/tasks/llm/types |
| Behavior | Detail |
|---|---|
| User IDs lowercased | upsertUser("MyUser") -> server stores "myuser". Then getUser("MyUser") returns 404. Always use lowercase. |
| Schedule names | Alphanumeric + underscores only. my-schedule rejected, my_schedule works. |
| Cron expressions | 6 fields including seconds: "0 0 0 1 1 *". Five-field cron "0 0 1 1 *" is rejected. |
| Empty task lists | tasks: [] in WorkflowDef is rejected. Minimum one task. |
| SIMPLE task state | Without a running worker polling, tasks stay SCHEDULED. For tests needing IN_PROGRESS without a worker, use WAIT task type instead. |
| Not-found responses | Some APIs (delete, get tags) return 200/empty for non-existent resources. Others throw 404. Error-path tests must handle both patterns. |
| Prompt models | savePrompt with models param needs "provider:model" format: ["openai:gpt-4o"] not ["gpt-4o"]. |
| Integration config | Provider types like "custom" may not exist. Use server-supported types ("openai"). Configuration needs api_key field. |
| Rate limit API | Uses raw HTTP PUT/GET/DELETE on /api/metadata/workflow/{name}/rate-limit. Not in OpenAPI spec. Not available on all server versions. |
| Workflow input expressions | Use ${workflow.input.fieldName} in inputParameters. Dollar-brace syntax, not template literals. |
Both return Workflow, both call the same underlying API. Key difference:
getWorkflow(id, includeTasks, retry=0)- has built-in retry on 500/404/403getExecution(id, includeTasks=true)- simple, no retry
Use getWorkflow when you need resilience. Use getExecution for straightforward one-shot fetches. There's also getWorkflowStatus() which returns WorkflowStatus (lighter, just status/output/variables).
MetricsCollector lazy-loads MetricsServer via await import("./MetricsServer.js"). The .js extension doesn't resolve in Jest. Always test MetricsServer by importing the class directly. Never test MetricsCollector's httpPort auto-start in Jest.
- Start workers BEFORE workflows. Worker polling must be active before you start a workflow, otherwise SIMPLE tasks have nothing to poll them.
- Independent workflow instances per test. Don't chain pause -> resume -> terminate on the same workflow across separate
test()blocks. If pause fails, resume/terminate cascade-fail. - Task scheduling delay. After completing task N in a multi-task workflow, task N+1 takes 1-3 seconds to become SCHEDULED. Poll in a loop, don't just
setTimeout(2000). clearWorkerRegistry()in afterEach. Worker tests register functions globally. Without clearing, workers leak between tests.
- Create
src/sdk/clients/<name>/<Name>Client.ts - Create
src/sdk/clients/<name>/index.ts:export * from "./<Name>Client" - Add
export * from "./<name>"tosrc/sdk/clients/index.ts - Add getter to
src/sdk/OrkesClients.ts:get<Name>Client(): <Name>Client { return new <Name>Client(this._client); }
- Write E2E tests:
src/integration-tests/<Name>Client.test.ts(CRUD + error paths)
- Create
src/sdk/builders/tasks/<taskName>.ts - Add
export * from "./<taskName>"tosrc/sdk/builders/tasks/index.ts - For LLM builders, put in
src/sdk/builders/tasks/llm/and export fromllm/index.ts
import { TaskType } from "../../../open-api";
import type { WorkflowTask } from "../../../open-api";
export const myNewTask = (
taskReferenceName: string,
name: string,
inputParameters: Record<string, unknown>,
optional?: boolean,
): WorkflowTask => ({
name,
taskReferenceName,
type: TaskType.MY_TYPE,
inputParameters,
optional,
});Test by building a workflow with ConductorWorkflow.add(), registering, and verifying the definition on the server via getWorkflowDef().
Co-located with source in __tests__/ directories.
npm test # All 469 unit tests
npm run test:unit # Unit tests onlyAgainst a real Conductor server. Location: src/integration-tests/.
# Full suite
CONDUCTOR_SERVER_URL=http://localhost:8080 \
CONDUCTOR_AUTH_KEY=key CONDUCTOR_AUTH_SECRET=secret \
ORKES_BACKEND_VERSION=5 \
npm run test:integration:orkes-v5
# Single file
npx jest --force-exit --testPathPatterns="AuthorizationClient"import { expect, describe, test, jest, beforeAll, afterAll } from "@jest/globals";
import { orkesConductorClient, OrkesClients, SomeClient } from "../sdk";
describe("SomeClient", () => {
jest.setTimeout(60000);
const suffix = Date.now();
let someClient: SomeClient;
const resourceName = `jssdktest_resource_${suffix}`; // lowercase, underscores only
beforeAll(async () => {
const client = await orkesConductorClient();
const clients = new OrkesClients(client);
someClient = clients.getSomeClient();
});
afterAll(async () => {
try { await someClient.delete(resourceName); } catch { /* ok */ }
});
test("create", async () => { ... });
test("get", async () => { ... });
describe("Error Paths", () => {
// Strict: API always 404s
test("get non-existent throws", async () => {
await expect(someClient.get("nonexistent")).rejects.toThrow();
});
// Tolerant: API may 200/empty or 404
test("delete non-existent throws or no-ops", async () => {
try { await someClient.delete("nonexistent"); }
catch (e) { expect(e).toBeDefined(); }
});
});
});Conventions:
- Import all jest globals from
"@jest/globals"(not globally available) - Resource names: lowercase + underscores +
Date.now()suffix - Cleanup: individual try/catch per operation in
afterAll - Timeouts:
jest.setTimeout(60000)minimum - Version gating:
describeForOrkesV5from./utils/customJestDescribe - Skip guards:
if (!featureSupported) return;for optional server features - Worker tests:
clearWorkerRegistry()inafterEach, start workers before workflows --force-exitflag: always use withnpx jest(some tests leave open handles)
| Layer | Tests | Notes |
|---|---|---|
| Unit | 469 | 97%+ on new code |
| E2E | 191 | 100% of all ~187 client methods |
| Error paths | 35 | Every client has negative tests |
| Total | 660 |
src/open-api/generated/ is auto-generated. Never edit directly.
npm run generate-openapi-layer # Regenerate from specTo extend types missing from the spec:
// In src/open-api/types.ts
import type { SomeType as OpenApiSomeType } from "./generated/types.gen";
export interface ExtendedSomeType extends OpenApiSomeType {
customField?: string; // Exists at runtime but not in spec
}Import patterns:
- Types:
import type { Workflow, Task } from "../open-api" - Resources:
import { WorkflowResource, TaskResource } from "../open-api/generated"
The types.ts file also defines key enums (TaskType with 28 variants, Consistency, ReturnStrategy, TaskResultStatusEnum) and specific typed task definitions (SimpleTaskDef, HttpTaskDef, etc.).
Read in resolveOrkesConfig.ts. Env vars take precedence over config object values.
| Variable | Purpose | Default |
|---|---|---|
CONDUCTOR_SERVER_URL |
Server URL (trailing / and /api auto-stripped) |
Required |
CONDUCTOR_AUTH_KEY |
API key ID | Required for auth |
CONDUCTOR_AUTH_SECRET |
API key secret | Required for auth |
CONDUCTOR_MAX_HTTP2_CONNECTIONS |
HTTP/2 connection pool size | - |
CONDUCTOR_REFRESH_TOKEN_INTERVAL |
Token refresh interval (ms) | 1200000 |
CONDUCTOR_REQUEST_TIMEOUT_MS |
Per-request timeout | 60000 |
CONDUCTOR_CONNECT_TIMEOUT_MS |
Connection timeout | 30000 |
CONDUCTOR_TLS_CERT_PATH |
Client TLS certificate | - |
CONDUCTOR_TLS_KEY_PATH |
Client TLS key | - |
CONDUCTOR_TLS_CA_PATH |
CA bundle | - |
CONDUCTOR_PROXY_URL |
HTTP/HTTPS proxy | - |
ORKES_BACKEND_VERSION |
Server version (test gating only) | - |
The examples/ directory contains 32 runnable TypeScript files across 3 subdirectories, matching the Python SDK's examples structure. See examples/README.md for the full catalog.
examples/
├── 13 core files # Workers, workflows, metrics, testing
├── agentic-workflows/ # 5 AI/LLM agent examples
├── api-journeys/ # 7 complete API lifecycle demos
└── advanced/ # 7 advanced workflow patterns
- Self-contained: Every file imports from
../src/sdk, connects viaOrkesClients.from(), and is runnable withnpx ts-node examples/<file>.ts - Cleanup: Examples that create resources clean up after themselves in try/finally blocks
- Env vars: All examples read
CONDUCTOR_SERVER_URL(required) and optionallyCONDUCTOR_AUTH_KEY/CONDUCTOR_AUTH_SECRET - AI examples: Use
LLM_PROVIDERandLLM_MODELenv vars with defaults toopenai_integration/gpt-4o - Naming: Kebab-case file names matching Python SDK's snake_case pattern (e.g.,
fork-join.ts↔fork_join_script.py) - Imports: Use relative paths from the example file (e.g.,
from "../../src/sdk"for subdirectory examples)
- Create the
.tsfile in the appropriate directory - Follow the standard pattern:
/** * Example Name — Brief description * * Demonstrates: feature1, feature2, feature3 * * Run: * CONDUCTOR_SERVER_URL=http://localhost:8080 npx ts-node examples/<file>.ts */ import { OrkesClients, ... } from "../src/sdk"; async function main() { const clients = await OrkesClients.from(); // ... example logic ... process.exit(0); } main().catch((err) => { console.error(err); process.exit(1); });
- Add to
examples/README.mdin the appropriate category table - If it's a key example, add to the root
README.mdexamples table
| JS Example | Python Equivalent | Notes |
|---|---|---|
workers-e2e.ts |
workers_e2e.py |
Multiple workers chained |
kitchensink.ts |
kitchensink.py |
All task types |
workflow-ops.ts |
workflow_ops.py |
Lifecycle operations |
dynamic-workflow.ts |
dynamic_workflow.py |
Programmatic builder |
task-context.ts |
task_context_example.py |
TaskContext usage |
metrics.ts |
metrics_example.py |
Prometheus metrics |
express-worker-service.ts |
fastapi_worker_service.py |
HTTP server + workers |
api-journeys/authorization.ts |
authorization_journey.py |
All authorization APIs |
api-journeys/metadata.ts |
metadata_journey.py |
All metadata APIs |
api-journeys/prompts.ts |
prompt_journey.py |
All prompt APIs |
api-journeys/schedules.ts |
schedule_journey.py |
All schedule APIs |
agentic-workflows/function-calling.ts |
agentic_workflows/function_calling_example.py |
LLM tool calling |
agentic-workflows/multiagent-chat.ts |
agentic_workflows/multiagent_chat.py |
Multi-agent debate |
advanced/rag-workflow.ts |
rag_workflow.py |
RAG pipeline |
advanced/fork-join.ts |
orkes/fork_join_script.py |
Parallel execution |
npm run build # tsup -> dist/ (ESM + CJS dual output)
npm run lint # ESLint
npm run lint-fix # ESLint auto-fix
npm run generate-docs # TypeDoc -> markdownNode 18+ required. Dual ESM/CJS via tsup with exports field in package.json.
| Feature | Python | JavaScript | Notes |
|---|---|---|---|
| Client factory | OrkesClients |
OrkesClients.from() |
Same pattern |
| 14 domain clients | All | All | Same method names |
| Workflow DSL | ConductorWorkflow |
ConductorWorkflow |
Fluent builder |
| Worker decorator | @worker_task |
@worker |
TS decorator syntax |
| Task context | get_task_context() |
getTaskContext() |
AsyncLocalStorage vs contextvars |
| Metrics | MetricsCollector |
MetricsCollector |
19 metric types, quantiles |
| Metrics server | HTTP endpoint | MetricsServer |
/metrics + /health |
| Non-retryable | NonRetryableError |
NonRetryableException |
FAILED_WITH_TERMINAL_ERROR |
| LLM builders | 13 builders | 13 builders | Full parity |
| Schema gen | @input_schema |
@schemaField / jsonSchema |
JSON Schema |
| Health monitor | Auto-restart | HealthMonitorConfig |
Backoff + restart |
| HTTP/2 | Optional | Optional (Undici) | Connection pooling |
| Signal API | signal() |
signal() + EnhancedSignalResponse |
All 4 return strategies |
| Rate limits | MetadataClient |
setWorkflowRateLimit |
Raw HTTP (not in spec) |
| Examples | 46 files, 6 subdirs | 32 files, 3 subdirs | JS merges some Python examples |