a9cd7750f4
CI / unit-test (push) Has been cancelled
CI / detect-changes (push) Has been cancelled
CI / build (push) Has been cancelled
Publish docs via GitHub Pages / Deploy docs (push) Has been cancelled
CI / test-harness (push) Has been cancelled
CI / generate-e2e-matrix (push) Has been cancelled
CI / e2e (push) Has been cancelled
CI / build-ui (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
UI v2 Integration CI / E2E (Integration) (push) Has been cancelled
UI v2 CI / Lint, Format & Test (push) Has been cancelled
UI v2 CI / E2E (Mocked) (push) Has been cancelled
366 lines
12 KiB
Markdown
366 lines
12 KiB
Markdown
---
|
|
description: "Workflow as code — build code-first workflows dynamically in Python using the Conductor SDK. Conditional branching, loops, parallel execution, and runtime-generated dynamic workflows."
|
|
---
|
|
|
|
# Dynamic workflows in code
|
|
|
|
## Workflow as code
|
|
|
|
Conductor supports a code-first workflow approach — build workflows programmatically using the Python SDK instead of writing JSON by hand. This workflow as code pattern lets you chain tasks with the `>>` operator, add conditional logic, loops, and parallel branches — all in Python. Code-first workflows are ideal for dynamic workflows where the task graph is determined at runtime.
|
|
|
|
### Simple sequential workflow
|
|
|
|
Chain tasks with the `>>` operator. Worker functions decorated with `@worker_task` become reusable task building blocks.
|
|
|
|
```python
|
|
from conductor.client.workflow.conductor_workflow import ConductorWorkflow
|
|
from conductor.client.worker.worker_task import worker_task
|
|
|
|
|
|
@worker_task(task_definition_name='fetch_order')
|
|
def fetch_order(order_id: str) -> dict:
|
|
return {'order_id': order_id, 'amount': 99.99, 'item': 'Widget'}
|
|
|
|
|
|
@worker_task(task_definition_name='process_payment')
|
|
def process_payment(order_id: str, amount: float) -> dict:
|
|
return {'transaction_id': 'txn_abc123', 'status': 'charged'}
|
|
|
|
|
|
@worker_task(task_definition_name='ship_order')
|
|
def ship_order(order_id: str, transaction_id: str) -> dict:
|
|
return {'tracking': 'TRACK-456', 'carrier': 'FedEx'}
|
|
|
|
|
|
workflow = ConductorWorkflow(name='order_fulfillment', version=1, executor=executor)
|
|
|
|
fetch = fetch_order(task_ref_name='fetch', order_id=workflow.input('order_id'))
|
|
pay = process_payment(
|
|
task_ref_name='pay',
|
|
order_id=workflow.input('order_id'),
|
|
amount=fetch.output('amount'),
|
|
)
|
|
ship = ship_order(
|
|
task_ref_name='ship',
|
|
order_id=workflow.input('order_id'),
|
|
transaction_id=pay.output('transaction_id'),
|
|
)
|
|
|
|
workflow >> fetch >> pay >> ship
|
|
workflow.output_parameters({
|
|
'tracking': ship.output('tracking'),
|
|
'transaction_id': pay.output('transaction_id'),
|
|
})
|
|
workflow.register(overwrite=True)
|
|
```
|
|
|
|
---
|
|
|
|
### Conditional branching with Switch
|
|
|
|
Route execution based on task output or workflow input. Each case gets its own task chain.
|
|
|
|
```python
|
|
from conductor.client.workflow.conductor_workflow import ConductorWorkflow
|
|
from conductor.client.workflow.task.switch_task import SwitchTask
|
|
|
|
|
|
workflow = ConductorWorkflow(name='route_by_priority', version=1, executor=executor)
|
|
|
|
classify = classify_ticket(
|
|
task_ref_name='classify',
|
|
description=workflow.input('description'),
|
|
)
|
|
|
|
switch = SwitchTask(task_ref_name='priority_router', case_expression=classify.output('priority'))
|
|
|
|
# Each case is a list of tasks to execute
|
|
switch.switch_case('critical', [
|
|
page_oncall(task_ref_name='page', ticket_id=workflow.input('ticket_id')),
|
|
escalate(task_ref_name='escalate', ticket_id=workflow.input('ticket_id')),
|
|
])
|
|
switch.switch_case('high', [
|
|
assign_senior(task_ref_name='assign', ticket_id=workflow.input('ticket_id')),
|
|
])
|
|
switch.default_case([
|
|
add_to_backlog(task_ref_name='backlog', ticket_id=workflow.input('ticket_id')),
|
|
])
|
|
|
|
workflow >> classify >> switch
|
|
workflow.register(overwrite=True)
|
|
```
|
|
|
|
---
|
|
|
|
### Parallel execution with Fork/Join
|
|
|
|
Run independent tasks in parallel and wait for all to complete.
|
|
|
|
```python
|
|
from conductor.client.workflow.conductor_workflow import ConductorWorkflow
|
|
from conductor.client.workflow.task.fork_task import ForkTask
|
|
from conductor.client.workflow.task.join_task import JoinTask
|
|
|
|
|
|
workflow = ConductorWorkflow(name='parallel_enrichment', version=1, executor=executor)
|
|
|
|
# Define independent tasks
|
|
credit_check = check_credit(task_ref_name='credit', customer_id=workflow.input('customer_id'))
|
|
fraud_check = check_fraud(task_ref_name='fraud', customer_id=workflow.input('customer_id'))
|
|
kyc_check = check_kyc(task_ref_name='kyc', customer_id=workflow.input('customer_id'))
|
|
|
|
# Fork runs all branches in parallel
|
|
fork = ForkTask(
|
|
task_ref_name='parallel_checks',
|
|
forked_tasks=[
|
|
[credit_check],
|
|
[fraud_check],
|
|
[kyc_check],
|
|
],
|
|
)
|
|
|
|
# Join waits for all branches
|
|
join = JoinTask(task_ref_name='wait_all', join_on=['credit', 'fraud', 'kyc'])
|
|
|
|
# Merge results
|
|
decide = make_decision(
|
|
task_ref_name='decide',
|
|
credit_score=credit_check.output('score'),
|
|
fraud_risk=fraud_check.output('risk_level'),
|
|
kyc_status=kyc_check.output('status'),
|
|
)
|
|
|
|
workflow >> fork >> join >> decide
|
|
workflow.output_parameters({'decision': decide.output('result')})
|
|
workflow.register(overwrite=True)
|
|
```
|
|
|
|
---
|
|
|
|
### Loops with Do/While
|
|
|
|
Repeat a set of tasks until a condition is met — useful for polling, retries, or iterative AI agent loops.
|
|
|
|
```python
|
|
from conductor.client.workflow.conductor_workflow import ConductorWorkflow
|
|
from conductor.client.workflow.task.do_while_task import DoWhileTask
|
|
|
|
|
|
workflow = ConductorWorkflow(name='agent_loop', version=1, executor=executor)
|
|
|
|
# The task(s) to repeat each iteration
|
|
think = call_llm(
|
|
task_ref_name='think',
|
|
prompt=workflow.input('goal'),
|
|
)
|
|
act = execute_tool(
|
|
task_ref_name='act',
|
|
tool=think.output('tool'),
|
|
args=think.output('args'),
|
|
)
|
|
|
|
# Loop until the LLM says it's done (max 10 iterations)
|
|
loop = DoWhileTask(
|
|
task_ref_name='agent_loop',
|
|
termination_condition='if ($.act["output"]["done"] == true) { false; } else { true; }',
|
|
tasks=[think, act],
|
|
)
|
|
loop.input_parameters.update({'max_iterations': 10})
|
|
|
|
summarize = summarize_results(task_ref_name='summarize', results=act.output('results'))
|
|
|
|
workflow >> loop >> summarize
|
|
workflow.register(overwrite=True)
|
|
```
|
|
|
|
---
|
|
|
|
### HTTP + system tasks mixed with workers
|
|
|
|
Combine built-in system tasks (HTTP, Wait, JQ Transform) with custom workers — no extra deployment needed for system tasks.
|
|
|
|
{% raw %}
|
|
```python
|
|
from conductor.client.workflow.conductor_workflow import ConductorWorkflow
|
|
from conductor.client.workflow.task.http_task import HttpTask
|
|
from conductor.client.workflow.task.json_jq_task import JsonJQTask
|
|
from conductor.client.workflow.task.wait_task import WaitTask
|
|
|
|
|
|
workflow = ConductorWorkflow(name='data_pipeline', version=1, executor=executor)
|
|
|
|
# HTTP task — fetch data from an external API (no worker needed)
|
|
fetch = HttpTask(task_ref_name='fetch_data', http_input={
|
|
'uri': 'https://api.example.com/records',
|
|
'method': 'GET',
|
|
'headers': {'Authorization': ['Bearer ${workflow.input.api_key}']},
|
|
})
|
|
|
|
# JQ Transform — reshape the response (no worker needed)
|
|
transform = JsonJQTask(
|
|
task_ref_name='transform',
|
|
script='.body.records | map({id: .id, value: .metrics.total})',
|
|
)
|
|
transform.input_parameters.update({
|
|
'records': fetch.output('response.body'),
|
|
})
|
|
|
|
# Custom worker — run business logic
|
|
enrich = enrich_records(
|
|
task_ref_name='enrich',
|
|
records=transform.output('result'),
|
|
)
|
|
|
|
# Wait — pause for 5 seconds before the next step
|
|
cooldown = WaitTask(task_ref_name='cooldown', wait_for_seconds=5)
|
|
|
|
# Custom worker — store results
|
|
store = save_to_database(task_ref_name='store', records=enrich.output('enriched'))
|
|
|
|
workflow >> fetch >> transform >> enrich >> cooldown >> store
|
|
workflow.output_parameters({'stored': store.output('count')})
|
|
workflow.register(overwrite=True)
|
|
```
|
|
{% endraw %}
|
|
|
|
---
|
|
|
|
### Sub-workflows
|
|
|
|
Break large workflows into reusable pieces. A parent workflow invokes child workflows as tasks.
|
|
|
|
```python
|
|
from conductor.client.workflow.conductor_workflow import ConductorWorkflow
|
|
from conductor.client.workflow.task.sub_workflow_task import SubWorkflowTask
|
|
|
|
|
|
# Child workflow (registered separately)
|
|
child = ConductorWorkflow(name='process_single_item', version=1, executor=executor)
|
|
validate = validate_item(task_ref_name='validate', item=child.input('item'))
|
|
transform = transform_item(task_ref_name='transform', item=validate.output('validated'))
|
|
child >> validate >> transform
|
|
child.output_parameters({'result': transform.output('transformed')})
|
|
child.register(overwrite=True)
|
|
|
|
|
|
# Parent workflow invokes the child
|
|
parent = ConductorWorkflow(name='batch_processor', version=1, executor=executor)
|
|
|
|
prepare = prepare_batch(task_ref_name='prepare', batch_id=parent.input('batch_id'))
|
|
|
|
run_child = SubWorkflowTask(
|
|
task_ref_name='process_item',
|
|
workflow_name='process_single_item',
|
|
version=1,
|
|
)
|
|
run_child.input_parameters.update({'item': prepare.output('first_item')})
|
|
|
|
aggregate = aggregate_results(
|
|
task_ref_name='aggregate',
|
|
result=run_child.output('result'),
|
|
)
|
|
|
|
parent >> prepare >> run_child >> aggregate
|
|
parent.register(overwrite=True)
|
|
```
|
|
|
|
---
|
|
|
|
### Runtime-generated dynamic workflow
|
|
|
|
Build a workflow definition at runtime and execute it without pre-registration. This runtime workflow pattern enables dynamic workflows where the task graph is generated on-the-fly — useful for AI agents, data pipelines, and any scenario where the steps are not known ahead of time.
|
|
|
|
{% raw %}
|
|
```python
|
|
from conductor.client.configuration.configuration import Configuration
|
|
from conductor.client.orkes_clients import OrkesClients
|
|
from conductor.client.http.models import StartWorkflowRequest
|
|
|
|
|
|
config = Configuration()
|
|
clients = OrkesClients(configuration=config)
|
|
executor = clients.get_workflow_executor()
|
|
|
|
# Build the workflow definition dynamically
|
|
steps = ['validate', 'enrich', 'store'] # determined at runtime
|
|
|
|
tasks = []
|
|
for i, step in enumerate(steps):
|
|
tasks.append({
|
|
'name': step,
|
|
'taskReferenceName': f'{step}_{i}',
|
|
'type': 'SIMPLE',
|
|
'inputParameters': {
|
|
'data': '${workflow.input.data}' if i == 0 else f'${{{steps[i-1]}_{i-1}.output.result}}',
|
|
},
|
|
})
|
|
|
|
# Start with inline definition — no pre-registration needed
|
|
request = StartWorkflowRequest(
|
|
name='dynamic_pipeline',
|
|
workflow_def={
|
|
'name': 'dynamic_pipeline',
|
|
'version': 1,
|
|
'tasks': tasks,
|
|
'outputParameters': {
|
|
'result': f'${{{steps[-1]}_{len(steps)-1}.output.result}}',
|
|
},
|
|
},
|
|
input={'data': {'key': 'value'}},
|
|
)
|
|
|
|
workflow_id = executor.start_workflow(request)
|
|
print(f'Started dynamic workflow: {workflow_id}')
|
|
```
|
|
{% endraw %}
|
|
|
|
This pattern is powerful for AI agents that generate execution plans at runtime — the LLM produces the list of steps, your code builds the workflow definition, and Conductor executes it with full durability, retries, and observability.
|
|
|
|
---
|
|
|
|
### Execute and wait for result
|
|
|
|
Run a workflow synchronously and get the result inline — useful for APIs and interactive applications.
|
|
|
|
```python
|
|
from conductor.client.configuration.configuration import Configuration
|
|
from conductor.client.orkes_clients import OrkesClients
|
|
|
|
config = Configuration()
|
|
clients = OrkesClients(configuration=config)
|
|
executor = clients.get_workflow_executor()
|
|
|
|
# Execute synchronously — blocks until the workflow completes
|
|
run = executor.execute(
|
|
name='order_fulfillment',
|
|
version=1,
|
|
workflow_input={'order_id': 'ORD-789'},
|
|
)
|
|
|
|
print(f'Status: {run.status}')
|
|
print(f'Output: {run.output}')
|
|
print(f'View: {config.ui_host}/execution/{run.workflow_id}')
|
|
```
|
|
|
|
---
|
|
|
|
## Setup
|
|
|
|
All examples above assume a `WorkflowExecutor` instance. Here is the standard setup:
|
|
|
|
```python
|
|
from conductor.client.configuration.configuration import Configuration
|
|
from conductor.client.orkes_clients import OrkesClients
|
|
|
|
config = Configuration() # reads CONDUCTOR_SERVER_URL from env
|
|
clients = OrkesClients(configuration=config)
|
|
executor = clients.get_workflow_executor()
|
|
```
|
|
|
|
```shell
|
|
pip install conductor-python
|
|
export CONDUCTOR_SERVER_URL=http://localhost:8080/api
|
|
```
|
|
|
|
For more Python SDK examples, see the [Python SDK documentation](../../documentation/clientsdks/python-sdk.md) and the [examples on GitHub](https://github.com/conductor-oss/python-sdk/tree/main/examples).
|