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.
Patterns
Section titled âPatternsâWhen to use which pattern
Section titled âWhen to use which patternâ| Pattern | Use when⌠| Example |
|---|---|---|
| Profile agent | A single AI call with execution tracking and composability | Classify a support ticket, answer a question |
| Sequential workflow | Steps must run in a fixed order, each building on the previous | Classify text, then summarize based on category |
| Loop workflow | You need iterative refinement until a quality threshold is met | Generate a haiku, rate it, improve it, repeat |
| Orchestrator | A central AI must decide which agents to call, and in what order | Travel planner delegating to calendar, flight, and hotel agents |
| Agent as tool | A specialist must be one option among the normal tools of a caller | A support assistant asks a product research agent for catalog data |
| Handoff | One specialist must take over and answer the user directly | Support triage routing to product, shipping, or account specialists |
| Round-robin conversation | Multiple perspectives must take turns in a fixed order | Brainstormer, critic, and synthesizer collaborate on a plan |
| AI-driven conversation | A moderator must decide who speaks next | Panel discussion where the moderator picks the most relevant expert |
Core concepts
Section titled âCore conceptsâ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.
Agent types
Section titled âAgent typesâ| Type | Constant | Purpose |
|---|---|---|
| Profile | c_type_profile | Wraps a prompt profile |
| Workflow | c_type_workflow | Sequential or loop execution of steps |
| Orchestrator | c_type_orchestrator | AI-driven delegation to specialized agents |
| Handoff | c_type_handoff | Agents transfer control to each other |
| Conversation | c_type_conversation | Multiple agents collaborate in dialogue |
Executing agents
Section titled âExecuting agentsâ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.
Conversation continuation
Section titled âConversation continuationâ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.
Binding a run to a value
Section titled âBinding a run to a valueâ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.
Input mapping
Section titled âInput mappingâ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" } }}Setting up prompt profiles
Section titled âSetting up prompt profilesâ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;/Structured output in agents
Section titled âStructured output in agentsâ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.
Removing an agent
Section titled âRemoving an agentâ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:
| Kept | Why |
|---|---|
| The prompt profile | A profile lives without an agent, and other agents can use it |
A shared or global memory store | More than one agent reads it |
A context memory store under a shared store_code | The namespace belongs to a group of agents |
| A session another agent opened | It is the conversation of that agent |
| A tool that runs the agent | A 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.
Session tracking and debugging
Section titled âSession tracking and debuggingâ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;Caller environment context
Section titled âCaller environment contextâ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.
| Column | Content |
|---|---|
created_by | APEX user if called from an APEX session, otherwise the database user |
audience | Caller 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_address | SYS_CONTEXT('USERENV', ...) values of the caller |
module, action, client_identifier, sid | Database session identification |
apex_user, apex_session_id, apex_app_id, apex_page_id | APEX session details (null when not called from a real APEX session) |
env_context | JSON 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;/Conversation sessions
Section titled âConversation sessionsâ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.
| Column | Content |
|---|---|
session_id | Primary key. The same ID that every execution row of the conversation carries |
root_agent_id | Agent that opened the session (the first top-level turn) |
title | Conversation title. Your front end sets it with set_session_title |
feedback_rating, feedback_comment, feedback_at | Verdict of the end user. Your front end sets it with set_session_feedback |
status | Status of the most recent top-level turn |
turn_count, message_count | Maintained counts of top-level turns and of message rows |
total_input_tokens, total_output_tokens | Sum of the own tokens of every run in the session |
started_at, last_activity_at, updated_at | First activity, most recent activity, and last change |
created_by, db_user, apex_user, audience, apex_*, env_context | Caller 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;/Naming a conversation
Section titled âNaming a conversationâ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.
Rating a conversation
Section titled âRating a conversationâ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.
Conversation message log
Section titled âConversation message logâ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.
| Column | Content |
|---|---|
seq | Order inside the session (a running max(seq)+1). The values are contiguous across turns |
role | user, assistant, tool_call, tool_result, reasoning, or system |
agent_code | The agent that produced this message (see below). It is null for the user and system input of the caller |
content | Message text (for text/reasoning, and for plain-string messages) |
tool_name, tool_input, tool_output, tool_status | Populated 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 (
userandsystem) has no attribution.agent_codeisnull.
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;Best practices
Section titled âBest practicesâ- 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, andmax_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: 2ormax_iterations: 2. Validate the flow, then increase the limits