Run Your First Workflow
See a workflow execute in 2 minutes. Build your own in 5.
You need Node.js (v16+) and Java 21+ installed. That's it.
Phase 1: See it work
Prerequisite: Java 21+ is required to run the Conductor server. Run
java --versionto check. Install Java 21 if needed.
Start Conductor
Wait for the server to start, then open the UI at http://localhost:8080.
Troubleshooting
- "Java not found" or server won't start? Install Java 21+ and make sure
java -versionshows 21 or higher. - Port 8080 already in use? Start on a different port:
conductor server start --port 9090 - Prefer Docker? Skip the CLI server and run:
docker run -p 8080:8080 conductoross/conductor:latest
Define the workflow
Save workflow.json — a two-task workflow that calls an API and parses the response, all server-side:
{
"name": "hello_workflow",
"version": 1,
"tasks": [
{
"name": "fetch_data",
"taskReferenceName": "fetch_ref",
"type": "HTTP",
"inputParameters": {
"http_request": {
"uri": "https://orkes-api-tester.orkesconductor.com/api",
"method": "GET"
}
}
},
{
"name": "parse_response",
"taskReferenceName": "parse_ref",
"type": "INLINE",
"inputParameters": {
"data": "${fetch_ref.output.response.body}",
"evaluatorType": "graaljs",
"expression": "(function() { var d = $.data; return { summary: 'Host ' + d.hostName + ' responded in ' + d.apiRandomDelay + ' with random value ' + d.randomInt, host: d.hostName, randomValue: d.randomInt }; })()"
}
}
],
"outputParameters": {
"summary": "${parse_ref.output.result.summary}",
"apiResponse": "${fetch_ref.output.response.body}"
},
"schemaVersion": 2,
"ownerEmail": "dev@example.com"
}
What's happening here:
fetch_data— an HTTP task that calls an external API. No worker needed.parse_response— an Inline task that runs JavaScript server-side to extract and summarize the API response.- Both are system tasks — Conductor executes them directly. No external code to deploy.
Register and run
Register the workflow:
Start the workflow:
The --sync flag waits for completion and prints the full workflow execution JSON to stdout. The CLI also prints a server detection line first, so actual output looks like:
Auto-detected server: http://localhost:8080
{"workflowId":"3e2a1c9d-...","status":"COMPLETED","tasks":[{...},{...}],"outputParameters":{"summary":"Host orkes-api-sampler-... responded in 0 ms with random value 1141","apiResponse":{...}},...}
To extract just the output in a readable form, pipe through jq:
{
"summary": "Host orkes-api-sampler-... responded in 0 ms with random value 1141",
"apiResponse": {
"randomString": "gbgkaofnvesptvlmocpk",
"randomInt": 1141,
"hostName": "orkes-api-sampler-...",
"apiRandomDelay": "0 ms",
"sleepFor": "0 ms",
"statusCode": "200",
"queryParams": {}
}
}
Note: The
Auto-detected server:line is written to stdout (not stderr). Usetail -1to skip it before piping tojq, and2>/dev/nullto suppress any stderr.
Open http://localhost:8080 to see the execution visually — the task timeline, inputs/outputs, and status of each step.
What just happened
Conductor called an external API, passed the response to server-side JavaScript for parsing, tracked every step, and would have retried on failure — all without writing or deploying any worker code.
Phase 2: Add a worker
Now write real code that Conductor orchestrates — with automatic retries.
Update the workflow
Save workflow-v2.json — adds a worker task that processes the parsed data:
{
"name": "hello_workflow",
"version": 2,
"tasks": [
{
"name": "fetch_data",
"taskReferenceName": "fetch_ref",
"type": "HTTP",
"inputParameters": {
"http_request": {
"uri": "https://orkes-api-tester.orkesconductor.com/api",
"method": "GET"
}
}
},
{
"name": "parse_response",
"taskReferenceName": "parse_ref",
"type": "INLINE",
"inputParameters": {
"data": "${fetch_ref.output.response.body}",
"evaluatorType": "graaljs",
"expression": "(function() { var d = $.data; return { summary: 'Host ' + d.hostName + ' responded in ' + d.apiRandomDelay + ' with random value ' + d.randomInt, host: d.hostName, randomValue: d.randomInt }; })()"
}
},
{
"name": "process_result",
"taskReferenceName": "process_ref",
"type": "SIMPLE",
"inputParameters": {
"summary": "${parse_ref.output.result.summary}",
"randomValue": "${parse_ref.output.result.randomValue}"
}
}
],
"outputParameters": {
"finalResult": "${process_ref.output.result}"
},
"schemaVersion": 2,
"ownerEmail": "dev@example.com"
}
Register the updated workflow and task definition:
curl -X POST http://localhost:8080/api/metadata/taskdefs \
-H 'Content-Type: application/json' \
-d '[{
"name": "process_result",
"retryCount": 2,
"retryLogic": "FIXED",
"retryDelaySeconds": 1,
"responseTimeoutSeconds": 10,
"ownerEmail": "dev@example.com"
}]'
Write the worker
Save worker.py:
import threading
from conductor.client.automator.task_handler import TaskHandler
from conductor.client.configuration.configuration import Configuration
from conductor.client.worker.worker_task import worker_task
@worker_task(task_definition_name="process_result")
def process_result(task) -> dict:
summary = task.input_data.get("summary", "")
random_value = task.input_data.get("randomValue", 0)
# Fail on first attempt to demonstrate retries
if task.retry_count == 0:
raise Exception(f"Simulated failure processing: {summary}")
return {
"result": summary.upper(),
"doubled": random_value * 2,
"attempt": task.retry_count + 1,
}
def main():
config = Configuration(server_api_url="http://localhost:8080/api")
handler = TaskHandler(configuration=config)
handler.start_processes()
try:
threading.Event().wait() # block until KeyboardInterrupt; no busy-wait
except KeyboardInterrupt:
handler.stop_processes()
if __name__ == "__main__":
main()
Install and run:
Start the workflow and watch retries
In a separate terminal:
In the terminal running your worker, you'll see:
Simulated failure processing: Host orkes-api-sampler-... responded in 0 ms with random value 1141
...
# 1 second later, the retry succeeds
Expected output:
{
"finalResult": {
"result": "HOST ORKES-API-SAMPLER-... RESPONDED IN 0 MS WITH RANDOM VALUE 1141",
"doubled": 2282,
"attempt": 2
}
}
Open http://localhost:8080 to see the retry visually in the execution diagram.
What just happened
Your worker failed, Conductor retried it after 1 second, and the retry succeeded. This is durable execution — Conductor manages retries so your code doesn't have to.
Phase 3: Replay a workflow
Every Conductor workflow execution is fully replayable — restart from the beginning, rerun from a specific task, or retry the failed step. This works on any workflow, at any time, even months after the original execution.
Restart from the beginning
Take any workflow execution ID from Phase 1 or Phase 2 and restart it:
# Start a workflow and capture its ID (printed as a plain UUID)
WORKFLOW_ID=$(conductor workflow start -w hello_workflow --version 2)
# Restart the entire workflow from the beginning
curl -X POST "http://localhost:8080/api/workflow/$WORKFLOW_ID/restart"
The workflow re-executes all tasks from scratch, creating a new execution trace while preserving the original.
Retry from the failed task
If a workflow failed (like the simulated failure in Phase 2), you can retry just the failed task instead of re-running everything:
# Retry from the last failed task
curl -X POST "http://localhost:8080/api/workflow/$WORKFLOW_ID/retry"
Conductor picks up from the failed task, reusing the outputs of all previously completed tasks.
What just happened
You replayed a workflow execution using two different strategies — full restart and retry from failure. Conductor preserved the full execution history, so you could replay at any time. This works on completed, failed, or timed-out workflows, indefinitely.
Workers in other languages
@WorkerTask("process_result")
public Map<String, Object> processResult(Map<String, Object> input) {
String summary = (String) input.get("summary");
int randomValue = (int) input.get("randomValue");
return Map.of(
"result", summary.toUpperCase(),
"doubled", randomValue * 2
);
}
See the Java SDK for full setup.
const { OrkesClients, TaskHandler } = require("@io-orkes/conductor-javascript");
async function main() {
const clients = await OrkesClients.from({ serverUrl: "http://localhost:8080/api" });
const handler = new TaskHandler(clients, [{
taskDefName: "process_result",
execute: async (task) => {
const { summary, randomValue } = task.inputData;
return { outputData: { result: summary.toUpperCase(), doubled: randomValue * 2 }, status: "COMPLETED" };
},
}]);
handler.startPolling();
}
main();
See the JavaScript SDK for full setup.
func ProcessResult(task *model.Task) (interface{}, error) {
summary := task.InputData["summary"].(string)
randomValue := int(task.InputData["randomValue"].(float64))
return map[string]interface{}{
"result": strings.ToUpper(summary),
"doubled": randomValue * 2,
}, nil
}
See the Go SDK for full setup.
[WorkerTask("process_result")]
public static TaskResult ProcessResult(Task task)
{
var summary = task.InputData["summary"].ToString();
var randomValue = (int)task.InputData["randomValue"];
return task.Completed(new {
result = summary.ToUpper(),
doubled = randomValue * 2
});
}
See the C# SDK for full setup.
Cleanup
Using Docker instead
If you prefer Docker over the CLI, you can run Conductor with:
All the workflow commands above work the same — just replace the CLI commands with their cURL equivalents:
| CLI | cURL |
|---|---|
conductor workflow create workflow.json |
curl -X POST http://localhost:8080/api/metadata/workflow -H 'Content-Type: application/json' -d @workflow.json |
conductor workflow start -w hello_workflow --sync |
curl -s -X POST "http://localhost:8080/api/workflow/execute/hello_workflow/1?waitForSeconds=10" -H 'Content-Type: application/json' -d '{}' |
conductor server stop |
docker rm -f conductor |
For production deployment options, see Running with Docker.
Next steps
- System tasks — HTTP, Wait, Event tasks without workers
- Operators — Fork/join, switch, loops, sub-workflows
- Error handling — Saga pattern, compensation flows
- Client SDKs — Java, Python, Go, C#, JavaScript, and more