--- 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).