Skip to content

About multi-agent systems

Multi-agent systems let you coordinate multiple specialized AI agents to solve complex tasks that are too much for a single prompt.

One large prompt is not necessary. You break the problem into smaller steps, and a specialized agent handles each step. Each agent has a prompt profile, and it can have its own model, provider, and instructions. UC AI then runs the agents in order. It manages the data flow, the conversation history, and the termination logic.

PatternUse when…Example
Profile agentA single AI call with execution tracking and composabilityClassify a support ticket, answer a question
Sequential workflowSteps must run in a fixed order, each building on the previousClassify text, then summarize based on category
Loop workflowYou need iterative refinement until a quality threshold is metGenerate a haiku, rate it, improve it, repeat
OrchestratorA central AI must decide which agents to call, and in what orderTravel planner delegating to calendar, flight, and hotel agents
Agent as toolA specialist must be one option among the normal tools of a callerA support assistant asks a product research agent for catalog data
HandoffOne specialist must take over and answer the user directlySupport triage routing to product, shipping, or account specialists
Round-robin conversationMultiple perspectives must take turns in a fixed orderBrainstormer, critic, and synthesizer collaborate on a plan
AI-driven conversationA moderator must decide who speaks nextPanel discussion where the moderator picks the most relevant expert

An agent is a reusable unit of AI work. The simplest agent wraps a prompt profile:

DECLARE
l_agent_id NUMBER;
BEGIN
l_agent_id := uc_ai_agents_api.create_agent(
p_code => 'text_classifier',
p_description => 'Classifies text into categories',
p_agent_type => uc_ai_agents_api.c_type_profile,
p_prompt_profile_code => 'TEXT_CLASSIFIER_PROFILE',
p_status => uc_ai_agents_api.c_status_active
);
END;
/

A profile agent runs its prompt profile on each call. You can then compose these agents into higher-level patterns.

TypeConstantPurpose
Profilec_type_profileWraps a prompt profile
Workflowc_type_workflowSequential or loop execution of steps
Orchestratorc_type_orchestratorAI-driven delegation to specialized agents
Handoffc_type_handoffAgents transfer control to each other
Conversationc_type_conversationMultiple agents collaborate in dialogue

All agent types share the same execution API:

DECLARE
l_result json_object_t;
l_session_id VARCHAR2(100);
BEGIN
l_session_id := uc_ai_agents_api.generate_session_id;
l_result := uc_ai_agents_api.execute_agent(
p_agent_code => 'my_agent',
p_input_parameters => json_object_t('{"prompt": "Hello"}'),
p_session_id => l_session_id
);
DBMS_OUTPUT.PUT_LINE(l_result.get_clob('final_message'));
END;
/

p_session_id groups related runs. Use it to debug a run, and to track the token usage of all its agents.

A profile agent and an orchestrator agent support a multi-turn conversation. After the first call, send a follow-up message with p_follow_up_message. UC AI loads the earlier conversation history from the session, and appends your new message:

DECLARE
l_result json_object_t;
l_session_id VARCHAR2(100);
BEGIN
l_session_id := uc_ai_agents_api.generate_session_id;
-- Initial call
l_result := uc_ai_agents_api.execute_agent(
p_agent_code => 'my_agent',
p_input_parameters => json_object_t('{"prompt": "Explain PL/SQL cursors"}'),
p_session_id => l_session_id
);
DBMS_OUTPUT.PUT_LINE(l_result.get_clob('final_message'));
-- Follow-up (conversation continues automatically)
l_result := uc_ai_agents_api.execute_agent(
p_agent_code => 'my_agent',
p_follow_up_message => 'Can you show an example with bulk collect?',
p_session_id => l_session_id
);
DBMS_OUTPUT.PUT_LINE(l_result.get_clob('final_message'));
END;
/

Profile agents describes the history management and the configuration.

p_run_context binds name/value pairs to a run, for example the document that a conversation is about:

l_result := uc_ai_agents_api.execute_agent(
p_agent_code => 'DOC_AGENT',
p_session_id => l_session_id,
p_run_context => json_object_t('{"document_id": "7"}')
);

Three things then use these values:

  • Every tool in the run receives them under the reserved key _ctx. Read the run context.
  • A {document_id} placeholder in the prompt profile resolves from them.
  • Agent memory can keep one store per value.

Every nested run inherits the context. A workflow step, an orchestrator delegate, a handoff target, and an agent as tool all work on the same document, and none of them can drop the binding.

The first turn binds the context to the session. A later turn of that session inherits it, so a follow-up turn does not have to pass it again. A later turn can add a new key. It cannot change a key that is already bound: UC AI raises -20507. To work on another document, start another session.

A key with the JSON value null is not bound. A later turn can give it a value, and a later null never changes a key that has one.

UC AI records the effective context in uc_ai_agent_executions.run_context and in uc_ai_agent_sessions.run_context.

An input mapping defines how the data flows between agents. It uses a path in curly braces to reference a value in the workflow state:

  • {$.input.*} - original input parameters
  • {$.steps.<output_key>} - output from a previous step
  • {$.chat_history} - conversation history (for conversation patterns)
  • {$.agent_description} - the description of the current agent

Simple syntax maps a value directly:

{
"input_mapping": {
"question": "{$.input.question}",
"context": "{$.steps.step1_result}"
}
}

Extended syntax handles optional values and defaults:

{
"input_mapping": {
"feedback": {
"path": "{$.steps.reviewer.feedback}",
"optional": true
},
"temperature": {
"path": "{$.input.temperature}",
"default": "0.7"
}
}
}

All agent patterns build on prompt profiles. Each agent that calls an AI needs a prompt profile that defines its instructions, provider, and model.

DECLARE
l_profile_id NUMBER;
BEGIN
l_profile_id := uc_ai_prompt_profiles_api.create_prompt_profile(
p_code => 'my_agent_profile',
p_description => 'Agent that does X',
p_system_prompt_template => 'You are an assistant that does X.',
p_user_prompt_template => 'Do X with this input: {prompt}',
p_provider => uc_ai.c_provider_openai,
p_model => uc_ai_openai.c_model_gpt_5_6_luna,
p_status => uc_ai_prompt_profiles_api.c_status_active
);
COMMIT;
END;
/

Then wrap the profile in an agent:

DECLARE
l_agent_id NUMBER;
BEGIN
l_agent_id := uc_ai_agents_api.create_agent(
p_code => 'my_agent',
p_description => 'Agent that does X',
p_agent_type => uc_ai_agents_api.c_type_profile,
p_prompt_profile_code => 'my_agent_profile',
p_status => uc_ai_agents_api.c_status_active
);
END;
/

An agent can return a structured JSON answer with the response schema of its prompt profile. Use this when another agent or the workflow logic must parse the output. An example is a rating agent that returns a numeric score for a loop exit condition.

DECLARE
l_profile_id NUMBER;
l_schema CLOB := '{
"type": "object",
"properties": {
"quality": {
"type": "number",
"minimum": 1,
"maximum": 10
},
"feedback": {
"type": "string"
}
},
"required": ["quality", "feedback"]
}';
BEGIN
l_profile_id := uc_ai_prompt_profiles_api.create_prompt_profile(
p_code => 'rater_profile',
p_description => 'Rates content on a 1-10 scale',
p_system_prompt_template => 'You are a critic. Rate the given content.',
p_user_prompt_template => 'Rate this: {text}',
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;
/

A workflow can then reference the structured fields, for example in an exit condition:

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

The structured output guide has more information about response schemas.

An agent that has run cannot be deleted: its history points at it, and delete_agent therefore stops with ORA-02292. A deleted history cannot come back. You have three ways out.

Keep everything, hide the agent. Archiving is the usual choice. The agent stops resolving, and every row stays:

BEGIN
uc_ai_agents_api.change_status('my_agent', 1, uc_ai_agents_api.c_status_archived);
COMMIT;
END;
/

Delete a definition that never ran. delete_agent removes one version and nothing else.

Delete the agent and everything that is only its. purge_agent takes every version of the code, its runs, the runs started from them, its sessions, the messages of both, and its memory:

BEGIN
uc_ai_agents_api.purge_agent('my_agent');
COMMIT;
END;
/

What a purge keeps, because other things also use it:

KeptWhy
The prompt profileA profile lives without an agent, and other agents can use it
A shared or global memory storeMore than one agent reads it
A context memory store under a shared store_codeThe namespace belongs to a group of agents
A session another agent openedIt is the conversation of that agent
A tool that runs the agentA tool names the agent in its PL/SQL text only. Delete such a tool yourself

A purge stops when another agent references this one, as a delegate of an orchestrator, as a target of a handoff, or as a step of a workflow. Remove the reference first, or the other agent points at nothing.

UC AI records every agent run in the uc_ai_agent_executions table. Use the session ID to see the full trace:

SELECT agent_id,
status,
iteration_count,
tool_calls_count,
total_input_tokens,
total_output_tokens,
started_at,
completed_at
FROM uc_ai_agent_executions
WHERE session_id = :session_id
ORDER BY started_at;

Each run also records who started it, and from where. UC AI captures this context once, at the start of the top-level run. Every nested run shares it: a workflow step, an orchestrator sub-agent, a handoff hop, and an agent that runs as a tool.

ColumnContent
created_byAPEX user if called from an APEX session, otherwise the database user
audienceCaller class at the start of the run: public (anonymous APEX visitor), authenticated (logged-in APEX user), or db (database or job session)
db_user, os_user, host, ip_addressSYS_CONTEXT('USERENV', ...) values of the caller
module, action, client_identifier, sidDatabase session identification
apex_user, apex_session_id, apex_app_id, apex_page_idAPEX session details (null when not called from a real APEX session)
env_contextJSON with the full context snapshot (session_user, current_schema, os_user, client_identifier, client_info, ip_address, host, terminal, module, action, sid plus the APEX values)

Outside APEX, UC AI creates an internal APEX session named UC_AI_AGENT_EXEC. UC AI never records this synthetic session as the caller. The apex_* columns then stay null.

SELECT created_by,
apex_app_id,
apex_page_id,
e.env_context
FROM uc_ai_agent_executions e
WHERE session_id = :session_id;

You can also use the API:

DECLARE
l_details json_object_t;
BEGIN
l_details := uc_ai_agents_api.get_execution_details(p_execution_id => 42);
DBMS_OUTPUT.PUT_LINE(l_details.to_clob);
END;
/

uc_ai_agent_executions holds one row for each run. uc_ai_agent_sessions holds one row for each conversation. UC AI writes this header row with the first turn, and it maintains the row with each later turn.

ColumnContent
session_idPrimary key. The same ID that every execution row of the conversation carries
root_agent_idAgent that opened the session (the first top-level turn)
titleConversation title. Your front end sets it with set_session_title
feedback_rating, feedback_comment, feedback_atVerdict of the end user. Your front end sets it with set_session_feedback
statusStatus of the most recent top-level turn
turn_count, message_countMaintained counts of top-level turns and of message rows
total_input_tokens, total_output_tokensSum of the own tokens of every run in the session
started_at, last_activity_at, updated_atFirst activity, most recent activity, and last change
created_by, db_user, apex_user, audience, apex_*, env_contextCaller context of the opening turn. These are the columns of the table above

Each run records only the tokens of the LLM calls that it made itself. The session totals are therefore a true sum, and a nested sub-agent run does not double count.

list_sessions reads the headers. It gives a front end its conversation list directly:

DECLARE
l_sessions SYS_REFCURSOR;
BEGIN
l_sessions := uc_ai_agents_api.list_sessions(p_created_by => :APP_USER);
END;
/

get_session_messages reads the full transcript of one session, in seq order:

DECLARE
l_messages SYS_REFCURSOR;
BEGIN
l_messages := uc_ai_agents_api.get_session_messages(p_session_id => :session_id);
END;
/

Your front end decides how it makes a title. An LLM can summarize the first exchange, or the user can type a title. The engine never sets a title itself.

BEGIN
uc_ai_agents_api.set_session_title(
p_session_id => :session_id
, p_title => 'Invoice question from March'
, p_created_by => :APP_USER
);
END;
/

A second call overwrites the title, so this procedure also renames a conversation. p_created_by restricts the change to the user that opened the session.

set_session_feedback records the verdict of the end user. The rating is up or down. A comment is optional.

BEGIN
uc_ai_agents_api.set_session_feedback(
p_session_id => :session_id
, p_rating => 'up'
, p_comment => 'Answered in one turn.'
, p_created_by => :APP_USER
);
END;
/

A null rating withdraws the feedback. UC AI then clears the rating, the comment, and the timestamp together. A rating that UC AI does not know becomes null. A newer front end can therefore never break the check constraint.

uc_ai_agent_executions tracks who ran. The uc_ai_agent_messages table is the normalized transcript of what was said. It holds one row for each message content item, ordered by seq inside the session. Query this table to rebuild a chat, to build an audit trail, or to show a conversation in a UI.

UC AI writes this table once for each turn, with only the new messages. History-window trimming therefore never damages the stored record. That trimming controls only what UC AI sends to the model. Only the top-level turn stores its messages. A nested sub-agent run reaches the table through the result of its parent turn. This covers a workflow step, an orchestrator delegate, a handoff hop, and a conversation participant.

ColumnContent
seqOrder inside the session (a running max(seq)+1). The values are contiguous across turns
roleuser, assistant, tool_call, tool_result, reasoning, or system
agent_codeThe agent that produced this message (see below). It is null for the user and system input of the caller
contentMessage text (for text/reasoning, and for plain-string messages)
tool_name, tool_input, tool_output, tool_statusPopulated for tool_call / tool_result rows

Attribution (agent_code). In a multi-agent pattern, UC AI stores the whole turn against the execution_id of the top-level wrapper. agent_code therefore tells you which agent produced a row:

  • Wrapper patterns (conversation, handoff): each row names the sub-agent that produced it — the participant that spoke, or the specialist that answered the hop.
  • Direct patterns (profile, orchestrator, workflow): each row names the own agent of the turn.
  • Caller input (user and system) has no attribution. agent_code is null.
SELECT seq,
role,
agent_code,
tool_name,
SUBSTR(content, 1, 80) AS content
FROM uc_ai_agent_messages
WHERE session_id = :session_id
ORDER BY seq;
  • Start simple: begin with a profile agent. Compose workflows or conversations later, when the task grows
  • Write a clear agent description: an orchestrator uses the agent descriptions to decide which agent to call. A clear description improves the delegation
  • Set the limits: always set max_turns, max_iterations, and max_delegations. These limits stop a runaway run
  • Use structured output for a decision point: if an exit condition or the routing logic depends on the output of an agent, use a response schema. The values are then reliable and easy to parse
  • Match the model to the task: use a capable model for an orchestrator or a moderator, because it must make decisions. Use a faster model for a leaf agent with one narrow task
  • Test with small limits first: start with max_turns: 2 or max_iterations: 2. Validate the flow, then increase the limits