Workers execute SIMPLE tasks in a workflow. They poll the Conductor server for tasks, execute business logic, and report results back. Workers are stateless and idempotent.
Most “I need a worker for X” requests are actually built-in tasks. Walk SKILL.md Rule 6’s table before writing any worker code — if a built-in matches (LLM call, Kafka publish, PDF render, vector index/search, sub-workflow trigger, wait, human approval, JQ transform, fork/join, etc.), use it. A custom worker is the right answer only when no built-in covers the operation (e.g. an internal API, a proprietary system, business logic that doesn’t fit a generic task).
name exactly, and include the idempotency note.inputDataoutputDataAll official SDKs live under the conductor-oss GitHub org with the *-sdk naming convention:
| Language | Repo (WebFetch the README) | Package / Install |
|---|---|---|
| Python | github.com/conductor-oss/python-sdk | PyPI: conductor-python — pip install conductor-python |
| JavaScript / TypeScript | github.com/conductor-oss/javascript-sdk | npm: @io-orkes/conductor-javascript — npm install @io-orkes/conductor-javascript |
| Java | github.com/conductor-oss/java-sdk | Maven: org.conductoross:conductor-client (and conductor-client-spring for Spring Boot) |
| Go | github.com/conductor-oss/go-sdk | Go module path: go get github.com/conductor-sdk/conductor-go (the source repo is at conductor-oss/go-sdk but the Go import path retains the historical conductor-sdk/conductor-go name) |
| .NET (C#) | github.com/conductor-oss/csharp-sdk | NuGet: conductor-csharp — dotnet add package conductor-csharp |
| Ruby | github.com/conductor-oss/ruby-sdk | Gem: conductor_ruby (early version — confirm patterns from the repo) |
| Rust | github.com/conductor-oss/rust-sdk | crates.io: conductor-rust (early version — confirm patterns from the repo) |
All SDKs connect via the same env vars: CONDUCTOR_SERVER_URL, CONDUCTOR_AUTH_KEY, CONDUCTOR_AUTH_SECRET.
Why the WebFetch step matters. The Python/JS/Java/Go patterns below are stable across recent versions but the API surface still drifts (e.g. Python switched runner classes; Java’s
@WorkerTaskannotation gained options; Go module path moved org without changing the import). The .NET / Ruby / Rust SDKs are younger and their APIs shift more — fetch the README before scaffolding rather than trust an example below.
pip install conductor-python
from conductor.client.worker.worker_task import worker_task
@worker_task(task_definition_name='process_order')
def process_order(order_id: str, amount: float) -> dict:
# Your business logic here
return {'status': 'processed', 'order_id': order_id, 'total': amount * 1.1}
Function parameters are automatically mapped from the task’s inputParameters. The return value becomes the task’s outputData.
from conductor.client.automator.task_handler import TaskHandler
from conductor.client.configuration.configuration import Configuration
config = Configuration() # reads CONDUCTOR_SERVER_URL, CONDUCTOR_AUTH_KEY, CONDUCTOR_AUTH_SECRET
with TaskHandler(configuration=config, scan_for_annotated_workers=True) as handler:
handler.start_processes()
# Workers poll until stopped
npm install @io-orkes/conductor-javascript
import {
orkesConductorClient,
TaskManager,
} from "@io-orkes/conductor-javascript";
const client = await orkesConductorClient({
serverUrl: "http://localhost:8080/api",
});
const taskManager = new TaskManager(client, [
{
taskType: "process_order",
execute: async ({ inputData }) => {
return {
status: "COMPLETED",
outputData: {
status: "processed",
order_id: inputData.order_id,
},
};
},
},
]);
taskManager.startPolling();
Gradle:
implementation 'org.conductoross:conductor-client:5.0.0'
Maven:
<dependency>
<groupId>org.conductoross</groupId>
<artifactId>conductor-client</artifactId>
<version>5.0.0</version>
</dependency>
public class ProcessOrderWorker implements Worker {
@Override
public String getTaskDefName() {
return "process_order";
}
@Override
public TaskResult execute(Task task) {
String orderId = (String) task.getInputData().get("order_id");
TaskResult result = new TaskResult(task);
result.setStatus(TaskResult.Status.COMPLETED);
result.addOutputData("status", "processed");
result.addOutputData("order_id", orderId);
return result;
}
}
public class Workers {
@WorkerTask("process_order")
public Map<String, Object> processOrder(@InputParam("order_id") String orderId) {
return Map.of("status", "processed", "order_id", orderId);
}
}
ConductorClient client = ConductorClient.builder()
.basePath("http://localhost:8080/api")
.build();
TaskClient taskClient = new TaskClient(client);
new TaskRunnerConfigurer.Builder(taskClient, List.of(new ProcessOrderWorker()))
.withThreadCount(10)
.build()
.init();
go get github.com/conductor-sdk/conductor-go
package main
import (
"fmt"
"time"
"github.com/conductor-sdk/conductor-go/sdk/client"
"github.com/conductor-sdk/conductor-go/sdk/model"
"github.com/conductor-sdk/conductor-go/sdk/worker"
)
func ProcessOrder(task *model.Task) (interface{}, error) {
orderId := fmt.Sprintf("%v", task.InputData["order_id"])
return map[string]interface{}{
"status": "processed",
"order_id": orderId,
}, nil
}
func main() {
apiClient := client.NewAPIClientFromEnv()
taskRunner := worker.NewTaskRunnerWithApiClient(apiClient)
taskRunner.StartWorker("process_order", ProcessOrder, 1, time.Millisecond*100)
// Blocks and polls until stopped
select {}
}
In your workflow definition, use "type": "SIMPLE" and set "name" to the task type your worker polls for:
{
"name": "process_order",
"taskReferenceName": "process_order_ref",
"type": "SIMPLE",
"inputParameters": {
"order_id": "${workflow.input.order_id}",
"amount": "${workflow.input.amount}"
}
}
The worker registered for task type process_order will automatically pick up this task when the workflow reaches it.
responseTimeoutSeconds on task definitions so stuck tasks get rescheduled.FAILED status with a reasonForIncompletion message for graceful failures. Return FAILED_WITH_TERMINAL_ERROR to fail the task without retrying.