Skip to content

Workflows

Workflows run agents in a defined structure. You define the steps and their order. The run is deterministic.

Workflows reference profile agents by their agent_code to create deterministic pipelines.

Agents run one after another. UC AI stores the output of each step under its output_key. A later step reads this output with the {$.steps.<output_key>} syntax.

DECLARE
l_workflow_id NUMBER;
l_session_id VARCHAR2(100);
l_result json_object_t;
l_workflow_def CLOB;
BEGIN
-- Define the workflow
l_workflow_def := '{
"workflow_type": "sequential",
"steps": [
{
"agent_code": "math_agent",
"input_mapping": {
"question": "{$.input.question}"
},
"output_key": "step1_result"
},
{
"agent_code": "summarizer_agent",
"input_mapping": {
"text": "{$.steps.step1_result}"
},
"output_key": "step2_result"
}
]
}';
-- Create the workflow agent
l_workflow_id := uc_ai_agents_api.create_agent(
p_code => 'math_summary_pipeline',
p_description => 'Solves a math question then summarizes the answer',
p_agent_type => uc_ai_agents_api.c_type_workflow,
p_workflow_definition => l_workflow_def,
p_status => uc_ai_agents_api.c_status_active
);
commit; -- agents must be committed before they can be executed
-- Execute
l_session_id := uc_ai_agents_api.generate_session_id;
l_result := uc_ai_agents_api.execute_agent(
p_agent_code => 'math_summary_pipeline',
p_input_parameters => json_object_t('{"question": "What is 7 + 8?"}'),
p_session_id => l_session_id
);
DBMS_OUTPUT.PUT_LINE('Result: ' || l_result.get_clob('final_message'));
END;
/

How it works:

  1. math_agent receives {"question": "What is 7 + 8?"} from the input
  2. UC AI stores its output under step1_result
  3. summarizer_agent receives {"text": "<answer of the math agent>"} from the output of the previous step
  4. The final result is the output of the last step

Steps run in order, the same as in a sequential workflow. Each step can also have a condition. A step runs only when its condition is true. If the condition is false, UC AI skips the step.

The condition is a string with a PL/SQL boolean expression. UC AI first resolves each {$.path} placeholder against the workflow state. It then evaluates the expression. A placeholder resolves to plain text, so put single quotes around it for a comparison with a string value.

DECLARE
l_workflow_id NUMBER;
l_result json_object_t;
l_workflow_def CLOB;
BEGIN
l_workflow_def := q'[{
"workflow_type": "conditional",
"steps": [
{
"agent_code": "geography_agent",
"condition": "'{$.input.category}' = 'geography'",
"input_mapping": {
"question": "{$.input.text}"
},
"output_key": "geo_result"
},
{
"agent_code": "summarizer_agent",
"condition": "'{$.input.category}' = 'summary'",
"input_mapping": {
"text": "{$.input.text}"
},
"output_key": "sum_result"
}
]
}]';
l_workflow_id := uc_ai_agents_api.create_agent(
p_code => 'category_router',
p_description => 'Routes a request to the matching specialist agent',
p_agent_type => uc_ai_agents_api.c_type_workflow,
p_workflow_definition => l_workflow_def,
p_status => uc_ai_agents_api.c_status_active
);
commit; -- agents must be committed before they can be executed
l_result := uc_ai_agents_api.execute_agent(
p_agent_code => 'category_router',
p_input_parameters => json_object_t('{"category": "geography", "text": "What is the capital of France?"}'),
p_session_id => uc_ai_agents_api.generate_session_id
);
DBMS_OUTPUT.PUT_LINE('Result: ' || l_result.get_clob('final_message'));
END;
/

How it works:

  1. UC AI evaluates the condition of each step against the current workflow state before the step runs
  2. With category = 'geography', only geography_agent runs. UC AI skips the summarizer step
  3. Steps without a condition always run
  4. The final result is the output of the last step that ran

A condition can also reference the output of an earlier step. A common pattern starts with a classifier step without a condition, and a response schema. The later steps then use its structured output in their conditions:

"condition": "'{$.steps.classification.category}' = 'complaint'"

The condition is a PL/SQL expression, so every boolean expression works. Examples are a numeric comparison such as "{$.steps.classification.confidence} >= 0.8", and a function call.

Put single quotes around a string reference, as in '{$.steps.x}' above. Leave a numeric reference without quotes, as in {$.steps.x} >= 8. The same convention applies to exit_condition, and to an input_mapping entry with "is_plsql_expression": true.

Agents run in a loop. The loop ends at an exit condition, or at the maximum number of iterations. Use a loop to refine an output until it reaches a quality level.

DECLARE
l_workflow_id NUMBER;
l_result json_object_t;
l_workflow_def CLOB;
BEGIN
l_workflow_def := '{
"workflow_type": "loop",
"pre_steps": [
{
"agent_code": "haiku_creator_agent",
"input_mapping": {
"topic": "{$.input.topic}"
},
"output_key": "current_haiku"
}
],
"steps": [
{
"agent_code": "haiku_rater_agent",
"input_mapping": {
"haiku": "{$.steps.current_haiku}",
"topic": "{$.input.topic}"
},
"output_key": "haiku_rating"
},
{
"agent_code": "haiku_improver_agent",
"input_mapping": {
"topic": "{$.input.topic}",
"feedback": "{$.steps.haiku_rating.rating_feedback}",
"haiku": "{$.steps.current_haiku}"
},
"output_key": "current_haiku"
}
],
"post_steps": [
{
"agent_code": "haiku_translator_agent",
"input_mapping": {
"language": "german",
"haiku": "{$.steps.current_haiku}"
},
"output_key": "translated_haiku"
}
],
"loop_config": {
"max_iterations": 3,
"exit_condition": "{$.steps.haiku_rating.quality} >= 8"
},
"final_message": "{$.steps.translated_haiku}"
}';
l_workflow_id := uc_ai_agents_api.create_agent(
p_code => 'haiku_refinement',
p_description => 'Creates, refines, and translates a haiku',
p_agent_type => uc_ai_agents_api.c_type_workflow,
p_workflow_definition => l_workflow_def,
p_max_iterations => 3,
p_status => uc_ai_agents_api.c_status_active
);
commit; -- agents must be committed before they can be executed
l_result := uc_ai_agents_api.execute_agent(
p_agent_code => 'haiku_refinement',
p_input_parameters => json_object_t('{"topic": "Star Wars"}'),
p_session_id => uc_ai_agents_api.generate_session_id
);
DBMS_OUTPUT.PUT_LINE('Final haiku: ' || l_result.get_clob('final_message'));
END;
/

How it works:

  1. Pre-steps run once before the loop: the haiku creator generates an initial haiku
  2. Loop steps repeat: the rater scores the haiku, then the improver corrects it with this feedback
  3. The loop exits when quality >= 8 or after 3 iterations
  4. Post-steps run once after the loop: the translator converts the final haiku to German
  5. final_message defines which step output becomes the overall result

Using structured output for exit conditions

Section titled “Using structured output for exit conditions”

A loop exit condition usually depends on a structured value in the answer of an agent. The haiku rater above uses a response schema. It returns a JSON object with the fields quality and rating_feedback:

DECLARE
l_profile_id NUMBER;
l_schema CLOB := '{
"type": "object",
"properties": {
"quality": {
"type": "number",
"minimum": 1,
"maximum": 10,
"description": "Quality rating from 1 to 10"
},
"rating_feedback": {
"type": "string",
"description": "One-sentence feedback for improvement"
}
},
"required": ["quality", "rating_feedback"]
}';
BEGIN
l_profile_id := uc_ai_prompt_profiles_api.create_prompt_profile(
p_code => 'haiku_rater_profile',
p_description => 'Rates haikus on a 1-10 scale',
p_system_prompt_template => 'You are a haiku critic. Rate haikus based on form, imagery, and emotional impact.',
p_user_prompt_template => 'Rate this haiku about "{topic}": {haiku}',
p_provider => uc_ai.c_provider_openai,
p_model => uc_ai_openai.c_model_gpt_5_6_luna,
p_response_schema => l_schema,
p_status => uc_ai_prompt_profiles_api.c_status_active
);
COMMIT;
END;
/

The workflow references the structured fields directly:

"exit_condition": "{$.steps.haiku_rating.quality} >= 8"

For a simpler loop, put every step in steps. Use the optional flag for the first iteration, because the feedback does not exist yet:

{
"workflow_type": "loop",
"steps": [
{
"agent_code": "haiku_creator_agent",
"input_mapping": {
"topic": "{$.input.topic}",
"feedback": {
"path": "{$.steps.haiku_rating.rating_feedback}",
"optional": true
}
},
"output_key": "haiku_result"
},
{
"agent_code": "haiku_rater_agent",
"input_mapping": {
"haiku": "{$.steps.haiku_result}",
"topic": "{$.input.topic}"
},
"output_key": "haiku_rating"
}
],
"loop_config": {
"max_iterations": 3,
"exit_condition": "{$.steps.haiku_rating.quality} >= 8"
}
}

In the first iteration, feedback does not exist, so UC AI omits it. From the second iteration, UC AI passes the feedback of the rater to the creator.

Not every step needs an LLM. A PL/SQL step runs your own code inside a workflow. Use it to reshape the output of one agent for the next agent, to read a value from a table, to calculate, to format, or to gate the rest of the pipeline. It makes no model call, so it costs nothing and it is deterministic.

Set "step_type": "plsql" and give a plsql_function_call. This is a PL/SQL function body that returns a CLOB, the same as a tool. It can hold at most one bind variable, by convention :parameters. This variable receives the whole workflow state as a JSON CLOB: { "input": {...}, "steps": {...} }. A PL/SQL step has no input_mapping. The code reads what it needs from the state.

DECLARE
l_workflow_id NUMBER;
l_result json_object_t;
l_workflow_def CLOB;
BEGIN
l_workflow_def := q'[{
"workflow_type": "sequential",
"steps": [
{
"agent_code": "extractor_agent",
"input_mapping": { "text": "{$.input.document}" },
"output_key": "extracted"
},
{
"step_type": "plsql",
"plsql_function_call": "declare s json_object_t := json_object_t(:parameters); o json_object_t := json_object_t(); begin o.put('amount_eur', s.get_object('steps').get_object('extracted').get_number('amount') * 1.08); return o.to_clob; end;",
"output_key": "converted"
},
{
"agent_code": "report_agent",
"input_mapping": { "amount": "{$.steps.converted.amount_eur}" },
"output_key": "report"
}
]
}]';
l_workflow_id := uc_ai_agents_api.create_agent(
p_code => 'invoice_pipeline',
p_description => 'Extract, convert currency in PL/SQL, then report',
p_agent_type => uc_ai_agents_api.c_type_workflow,
p_workflow_definition => l_workflow_def,
p_status => uc_ai_agents_api.c_status_active
);
commit;
l_result := uc_ai_agents_api.execute_agent(
p_agent_code => 'invoice_pipeline',
p_input_parameters => json_object_t('{"document": "Invoice total: 100 USD"}'),
p_session_id => uc_ai_agents_api.generate_session_id
);
END;
/

Return value → real JSON type. UC AI parses the returned CLOB and stores it under output_key with its real type. A later step can therefore navigate it:

Snippet returnsStored asReferenced by
return '42';number 42{$.steps.key}
return o.to_clob; (a JSON object)object{$.steps.key.field}
return 'done';string "done"{$.steps.key}

UC AI stores text that is not valid JSON as a plain string.

output_key is optional for a PL/SQL step. Omit it for a step with only a side effect, for example a write to a table. The code must still return a value that is not null.

Gating the rest of the workflow. A PL/SQL step uses normal condition gating. It has its own condition, and a later step can gate on its output. A PL/SQL step can also return an object with "__control__": "stop". The workflow then stops at once, and no later step runs:

{
"step_type": "plsql",
"plsql_function_call": "declare s json_object_t := json_object_t(:parameters); o json_object_t := json_object_t(); begin if s.get_object('steps').get_object('review').get_number('score') < 3 then o.put('__control__', 'stop'); end if; return o.to_clob; end;",
"output_key": "gate"
}

A string value that resolves a path from the workflow state:

"question": "{$.input.question}"
PathDescription
{$.input.*}Original input parameters passed to the workflow
{$.steps.<output_key>}Full output of a previous step
{$.steps.<output_key>.<field>}One field from the structured output of a step

A workflow is a deterministic pipeline and not a conversation. The uc_ai_agent_messages transcript therefore holds a single assistant row with the output of the last step. Its agent_code names the workflow agent. The detail of each step lives in uc_ai_agent_executions. Each step runs as a child execution under the workflow wrapper. To see an intermediate step, query that table by parent_execution_id.

seqroleagent_codecontent
1assistanthaiku_workflowAn improved haiku: ...

A workflow writes a checkpoint of its state to uc_ai_agent_executions.current_state after every completed step. A conversation agent and a handoff agent write a checkpoint after every turn or handoff. UC AI commits each checkpoint in an autonomous transaction. You can therefore watch a long workflow from another session:

SELECT status,
json_value(current_state, '$._last_completed_step') AS last_step,
json_value(current_state, '$._checkpoint_at') AS checkpointed_at
FROM uc_ai_agent_executions
WHERE id = :execution_id;

The checkpoint contains the full workflow state (all step outputs so far) plus two markers:

  • _last_completed_step — agent code of the most recently completed step
  • _checkpoint_at — timestamp of the checkpoint

After a successful run, UC AI clears the checkpoint. The final state then lives in output_result. A failed or crashed run keeps its last checkpoint. You can therefore see how far the workflow came, and what each step returned. uc_ai_agents_api.get_execution_details also returns current_state when a checkpoint exists.