Decide
uc_ai.decide answers several questions about the same text or images in one call. You supply this content as the state. Each question requests a choice, a score, or the probability that a statement is true.
Use a decision to select the next action in your code. For example, classify a customer message as billing or support, then send it to that team. A choice question returns one of your defined options. A score question returns a number within your defined levels. The model also estimates probabilities, which your code can compare with a threshold before it acts.
Three providers support decisions:
| Provider | Constant | Endpoint | Default model |
|---|---|---|---|
| OpenAI | uc_ai.c_provider_openai | OpenAI Decisions (/decisions) | uc_ai_openai.c_model_gpt_6_luna |
| TypeSafe | uc_ai.c_provider_typesafe | TypeSafe jev (/systemone) | uc_ai_typesafe.c_model_jev_latest |
| OpenRouter | uc_ai.c_provider_openrouter | OpenRouter Decisions, alpha (/alpha/decisions) | uc_ai_openrouter.c_decision_model_jev |
Decisions do not go through generate_text. The result has no messages and no tool calls. A provider that is not in this table raises ORA-20306. TypeSafe supports decisions only. See the TypeSafe provider page.
The OpenRouter endpoint uses the same request and answer formats as TypeSafe. OpenRouter sends the request to a provider that it lists for the model, so use a model that supports decisions. The default model is typesafe/jev-1.13. A model that does not exist, or that has no decision support, raises ORA-20302 with HTTP 400: Model <name> does not exist. The call to the default model on 2026-10-08 returned the model typesafe/jev-1.13-20260917.
Signature
Section titled “Signature”function decide ( p_state in clob, p_questions in json_object_t, p_provider in provider_type, p_model in model_type default null, p_files in json_array_t default null, p_options in json_object_t default null) return json_object_t;| Parameter | Type | Required | Description |
|---|---|---|---|
p_state | clob | Yes, if there is no file | The text that the questions refer to. It can be JSON that you serialize with to_clob. The provider reads JSON as text. |
p_questions | json_object_t | Yes | The questions, keyed by question id. See Questions. |
p_provider | provider_type | Yes | uc_ai.c_provider_openai, uc_ai.c_provider_typesafe or uc_ai.c_provider_openrouter. |
p_model | model_type | No | null selects the default model of the provider. |
p_files | json_array_t | No | Images, made with uc_ai_message_api.create_file_content. See Images. |
p_options | json_object_t | No | Provider options. See The p_options parameter. |
An empty or null p_state without a file raises ORA-20503.
Questions
Section titled “Questions”p_questions is a JSON object. Each key is a question id, and each value is the definition of one question. The answers use the same ids.
{ "department": { "type": "choice", "instructions": "Which team should handle this message?", "criteria": { "billing": "Payments, invoices, and refunds.", "support": "Product usage and technical problems." } }, "severity": { "type": "score", "instructions": "How severe is the problem described?", "criteria": ["Cosmetic", "Workaround exists", "Blocking"] }, "wants_refund": { "type": "boolean", "instructions": "Does the customer ask for money back?" }}A question has these keys:
| Key | Required | Description |
|---|---|---|
type | Yes | boolean, choice or score. |
instructions | Yes | The text of the question. Blank instructions raise ORA-20503. |
criteria | Depends on the type | See the question types below. |
A question with any other key adds a warning, and UC AI ignores the key.
Question ids
Section titled “Question ids”A question id starts with a letter. It has up to 64 letters, digits or underscores. The pattern is ^[A-Za-z][A-Za-z0-9_]{0,63}$. UC AI checks every id before it sends a request.
Choice
Section titled “Choice”The answer is one of the options that you define. criteria is an object. Each key is an option, and each value is a description of the option. The question needs at least 2 options and at most 255 options. A description can be null or empty.
The answer is a number between the first and the last level. criteria is an array of level descriptions. Level 0 is the first element. The question needs at least 2 levels and at most 10 levels. A description can be null or empty.
Boolean
Section titled “Boolean”The answer is the probability that the statement is true. criteria is optional. It is an object with the keys true and false. Each value describes when the answer is true or false. Both keys are optional.
Result
Section titled “Result”decide returns a json_object_t with these keys:
| Key | Type | Description |
|---|---|---|
answers | object | One answer for each question, keyed by question id. See Answers. |
usage | object | Only the values that the provider reports: input_tokens, output_tokens and total_tokens. |
model | string | The model that the provider reports. If the provider reports none, the requested model. |
provider_metadata | object | confidence and usage. See Confidence. |
warnings | array of strings | Problems that did not stop the call. The array is empty when there are none. |
raw | object | The response of the provider as it came back. |
raw contains provider-specific fields that UC AI does not include in its common result structure. For example, a TypeSafe score can include a legend there.
Answers
Section titled “Answers”Each answer has a type. The type of an answer is the type of its question, or refused.
| Type | Keys | Description |
|---|---|---|
boolean | probability | The probability that the statement is true, from 0 to 1. |
choice | choice, probabilities | The chosen option. probabilities has the probability of every option. |
score | score, probabilities | The probability-weighted mean of the level indexes, rounded to 2 decimals. probabilities is keyed by the level index as a string ("0", "1", …). |
refused | None | The provider refused the question. See Refused answers. |
The providers return probabilities with 2 decimals. A probabilities object lists every option or level, including those with probability 0. Its keys can appear in a different order from your options. Use has to make sure that probabilities exists before you read it. This keeps your code compatible with providers that omit it.
Levels start at index 0. With three levels, the indexes are 0, 1, and 2. A score of 1.01 is close to the second level.
Refused answers
Section titled “Refused answers”OpenAI can refuse a question. It refuses each question on its own and answers the other questions of the same call. The refused question gets the answer {"type": "refused"}. The call does not raise an error. This is also true when OpenAI refuses every question.
Read the type of an answer before you read choice, score or probability. A refused answer has none of these keys.
OpenAI refused questions that have no clear answer. Examples are a question with meaningless instructions, and a question that infers a protected trait of a person. TypeSafe never refuses. It answers every question. A meaningless question gets a low confidence.
A refused question still costs the input tokens of the call.
Confidence
Section titled “Confidence”provider_metadata.confidence holds one number for each choice and score question, keyed by question id. The provider calculates it. It is not the probability of the chosen option. In the example below, OpenAI chose support with the probability 0.80 and the confidence 0.70.
Boolean questions and refused questions have no confidence. provider_metadata.usage holds the usage object of the provider as it came back.
Warnings
Section titled “Warnings”A warning describes a problem that did not stop the call. The call still returns a result. Each warning is a string. These cases add a warning:
- The probabilities of a question do not add up to 1.
- A score differs from the probability-weighted mean of its levels.
- A question has a key other than
type,instructionsandcriteria. p_optionshas a key that the provider does not use.
The providers round probabilities and scores to 2 decimals. Rounding can change a value by up to 0.005. UC AI allows for this difference.
The tolerance of the sum is 0.000001, plus 0.005 for each probability in the sum. The score tolerance starts at 0.000001, plus 0.005 for rounding the score. For each level, it also adds 0.005 multiplied by that level’s index.
The tolerance of the sum grows with the number of options. With about 200 options or more, this tolerance is too large to detect an incorrect sum. This check is useful for questions with fewer options.
Images
Section titled “Images”OpenAI and TypeSafe read images. Pass them in p_files, as objects from uc_ai_message_api.create_file_content. Any media type that the provider does not accept raises ORA-20508 before a request is sent.
- OpenAI accepts
image/png,image/jpegandimage/webp. UC AI sends each image as a Base64data:URL. - TypeSafe accepts
image/pngandimage/jpeg. UC AI sends each image in theassetsobject of the request and writes a reference to it at the end of the state, for exampleImage 1: uc-ai-image-1-<id>. The questions see the images through the state. UC AI also sends the headerTypeSafe-Danger-Zone-Images: true, which jev requires for images. - OpenRouter reads text only. A file in
p_filesraisesORA-20508.
Images on TypeSafe are a preview feature. TypeSafe can change the request format, the answers and the limits. Each image must be 256 to 1568 pixels wide and high, and at most 16 MiB. The request must be at most 32 MiB, and Base64 makes the images about one third larger.
UC AI does not check image dimensions. jev rejects an image outside these limits and does not resize it. The call then raises ORA-20302 with the message of jev.
Rotate the image itself upright before you send it. jev ignores the EXIF metadata that tells viewers how to rotate a photo. TypeSafe lists the limits on docs.typesafe.ai/images.
You can pass an image without text. If p_files holds an image, p_state can be empty. The call needs text, a file, or both.
Checks and errors
Section titled “Checks and errors”UC AI first makes sure that the provider supports decisions, then checks the input. If an input check fails, UC AI raises an error without a provider request.
| Code | When | Cause |
|---|---|---|
ORA-20306 | Before the request | p_provider is null, unknown, or has no decision support. The message lists the providers that have decision support. |
ORA-20503 | Before the request | A question or an id is malformed. A choice with fewer than 2 options. A score with fewer than 2 levels. An unknown question type. Blank instructions. An empty state without a file. A file object or a value in p_options of the wrong type. |
ORA-20508 | Before the request | The provider cannot take the call. More than 255 options in a choice. More than 10 levels in a score. An image type that the provider does not accept. A file for OpenRouter. |
ORA-20302 | After the request | The provider returned an error. The message holds the HTTP status and the message of the provider. |
ORA-20304 | After the response | The answers do not match the questions. |
ORA-20304 has these causes:
- A question has no answer, or the response contains an answer for an unknown question.
- An answer has no name, or two answers have the same name.
- The type of an answer is not the type of its question.
- A chosen option is not one of the options of the question.
- A score is outside the range of the defined levels.
- A probability is outside the range from 0 to 1.
A refused answer counts as a response to its question. UC AI does not require it to contain a choice, score, or probability.
If the probabilities do not add up, the call does not raise an error. It adds a warning.
The errors that UC AI raised for malformed calls on 2026-10-07:
ORA-20503: Invalid question id "1st": an id starts with a letter and has up to 64 letters, digits or underscoresORA-20503: Invalid question "dept": a choice question needs at least 2 options in criteria, found 1ORA-20503: Invalid question "dept": type "predicate" is not supported. Use boolean, choice or scoreORA-20503: Invalid question "dept": instructions are missing. Pass a non-empty stringORA-20503: Invalid state: it is null or empty and there is no file. Pass text, an image or bothORA-20306: Provider anthropic has no decision support. Decision providers: openai, typesafe, openrouterORA-20508: Unsupported content type: 256 options in question "big". The provider allows at most 255ORA-20508: Unsupported content type: files. The provider takes text only, pass the state as textORA-20508: Unsupported content type: media type application/pdf for file 1. Supported: image/png, image/jpeg, image/webpA model name that the provider does not know raises ORA-20302:
ORA-20302: Error response from provider OpenAI: HTTP 404: The model `gpt-6-nope` does not exist or you do not have access to it.ORA-20302: Error response from provider TypeSafe: HTTP 400: Unknown model: jev-nopeFor a validation error, TypeSafe adds the path of the field to the message. UC AI never copies the input of the request into an error message.
Limits
Section titled “Limits”| Limit | OpenAI | TypeSafe and OpenRouter |
|---|---|---|
| Options in a choice | 2 to 255 | 2 to 255 |
| Levels in a score | 2 to 10 | 2 to 10 |
| Questions in one call | At least 1 | At least 1 |
| Images | image/png, image/jpeg, image/webp | TypeSafe: image/png, image/jpeg. OpenRouter: none. |
The OpenRouter reference does not list limits for choices, levels or images. UC AI applies the TypeSafe limits to OpenRouter.
TypeSafe accepts a question with one option or one level. UC AI requires at least 2 options or levels for every provider. The upper limit for the number of questions is not known. A call with 50 questions worked on OpenAI.
The vendor documentation lists the size of the context. For TypeSafe, see docs.typesafe.ai. For OpenAI, see the OpenAI API reference.
The p_options parameter
Section titled “The p_options parameter”p_options is a JSON object with the two supported keys listed here. UC AI ignores other keys and adds a warning for each one.
| Key | Provider | Description |
|---|---|---|
safety_identifier | OpenAI | A string. UC AI sends it as it is. For TypeSafe, the key adds a warning and UC AI ignores it. |
extra_body | OpenAI, TypeSafe, OpenRouter | An object. UC AI merges its keys into the request body. The keys model, questions and input (OpenAI) or state (TypeSafe, OpenRouter) always keep the value from UC AI. |
A safety_identifier that is not a string, or an extra_body that is not an object, raises ORA-20503.
A call with {"safety_identifier":"user-123","temperature":0} to TypeSafe returned these warnings:
["Option \"safety_identifier\" is not supported for provider typesafe and was ignored","Option \"temperature\" is not supported for provider typesafe and was ignored"]Credentials and base URL
Section titled “Credentials and base URL”Decision calls use the configuration that generate_text reads from the package globals. The credential comes from uc_ai.g_apex_web_credential, or from the web credential of the provider:
| Provider | Web credential variable |
|---|---|
| OpenAI | uc_ai_openai.g_apex_web_credential |
| TypeSafe | uc_ai_typesafe.g_apex_web_credential |
| OpenRouter | uc_ai_openrouter.g_apex_web_credential |
If no web credential is set, UC AI reads the key with your function uc_ai_get_key and sends it as a Bearer header. See Set up API Keys.
uc_ai.g_base_url also applies to decision calls. If you set it, for example to a proxy, UC AI sends the decision calls to that URL as well. OpenAI receives <g_base_url>/decisions, TypeSafe receives <g_base_url>/systemone, and OpenRouter receives <g_base_url>/alpha/decisions. Without uc_ai.g_base_url, the URLs are https://api.openai.com/v1/decisions, https://api.typesafe.ai/v1/systemone and https://openrouter.ai/api/alpha/decisions. The base URL of OpenRouter has no /v1, because the decisions endpoint is not under /api/v1.
A decision call has a transfer timeout of 300 seconds. To change it, set uc_ai.g_transfer_timeout to a number of seconds.
A decision agent does not read these globals. It reads the credential, the base URL, and the transfer timeout (g_transfer_timeout) from the model_config of the agent.
Logging
Section titled “Logging”UC AI never writes the request body to the log. The body holds your text and the Base64 data of the images. The log holds the provider, the model, the number of questions, the number of files and the size in bytes.
Differences between the providers
Section titled “Differences between the providers”| OpenAI | TypeSafe | OpenRouter | |
|---|---|---|---|
| Endpoint | POST <base>/decisions | POST <base>/systemone | POST <base>/alpha/decisions |
| Default model | uc_ai_openai.c_model_gpt_6_luna | uc_ai_typesafe.c_model_jev_latest | uc_ai_openrouter.c_decision_model_jev |
model in the result | The name that you sent | The version that answered. jev-latest gave jev-1.13.0. | The model that OpenRouter reports in the response |
| Images | image/png, image/jpeg, image/webp | image/png, image/jpeg (preview) | None. A file raises ORA-20508. |
| Refusal | Per question. The answer has the type refused. | Never | Never. The response has no refusal field. |
| Boolean criteria | UC AI adds the texts to the instructions, because OpenAI takes no criteria for a boolean question | UC AI sends the criteria as they are | UC AI sends the criteria as they are |
confidence | Choice and score questions | Choice and score questions | Choice and score questions |
usage keys | input_tokens, output_tokens (always 0), total_tokens | input_tokens, output_tokens | input_tokens, output_tokens. The cost is in provider_metadata.usage. |
| Error body | An error object | A detail text, object or list | An error object with code and message |
For an OpenAI boolean question, UC AI appends the criteria to the question instructions. It adds a blank line, Criteria for true:, and the true description on the next line. It does the same for false. It omits a criterion if its description is empty.
Examples
Section titled “Examples”Route a support message
Section titled “Route a support message”The example asks three questions about one customer message. It reads the answers and routes the message. A message goes to a team only if the probability of the best option is at least 0.9. Otherwise a person reviews it.
Run the block as a user that has the web credential OPENAI. Replace the message and the credential with your own values. The block prints the answers, the usage, the metadata and the warnings. Then it prints the routing result.
declare c_threshold constant number := 0.9;
l_message clob := q'[Hello, the export button in your app does nothing since the last update. By the way, is there a discount for 10 licenses?]'; l_questions json_object_t := json_object_t(q'[{ "department": { "type": "choice", "instructions": "Which team should handle this message?", "criteria": { "billing": "Payments, invoices, and refunds.", "support": "Product usage and technical problems.", "sales": "New purchases and pricing questions." } }, "severity": { "type": "score", "instructions": "How severe is the problem described?", "criteria": [ "Appearance only; no lost functionality.", "Something is broken but the customer can continue.", "The customer cannot continue or lost money." ] }, "wants_refund": { "type": "boolean", "instructions": "Does the customer ask for money back?", "criteria": { "true": "The customer asks for a refund or a reimbursement.", "false": "The customer does not ask for money back." } } }]'); l_result json_object_t; l_answers json_object_t; l_dept json_object_t; l_choice varchar2(100 char); l_p number; l_keys json_key_list;begin uc_ai_openai.g_apex_web_credential := 'OPENAI';
l_result := uc_ai.decide( p_state => l_message , p_questions => l_questions , p_provider => uc_ai.c_provider_openai );
l_answers := l_result.get_object('answers'); l_keys := l_answers.get_keys; <<answer_loop>> for i in 1 .. l_keys.count loop sys.dbms_output.put_line(l_keys(i) || ': ' || l_answers.get_object(l_keys(i)).to_string); end loop answer_loop; sys.dbms_output.put_line('model: ' || l_result.get_string('model')); sys.dbms_output.put_line('usage: ' || l_result.get_object('usage').to_string); sys.dbms_output.put_line('provider_metadata: ' || l_result.get_object('provider_metadata').to_string); sys.dbms_output.put_line('warnings: ' || l_result.get_array('warnings').to_string);
l_dept := l_answers.get_object('department'); if l_dept.get_string('type') = 'refused' then sys.dbms_output.put_line('route: manual review (the provider refused the question)'); else l_choice := l_dept.get_string('choice'); l_p := l_dept.get_object('probabilities').get_number(l_choice); if l_p >= c_threshold then sys.dbms_output.put_line('route: ' || l_choice || ' (probability ' || to_char(l_p, 'FM0.00', 'NLS_NUMERIC_CHARACTERS=''.,''') || ')'); else sys.dbms_output.put_line('route: manual review (best option ' || l_choice || ', probability ' || to_char(l_p, 'FM0.00', 'NLS_NUMERIC_CHARACTERS=''.,''') || ')'); end if; end if;end;/The code checks type first. A refused department question has no choice. The code then routes by the probability of the chosen option. It does not use confidence for this.
Select a provider to see its output. The output of the two providers differs, because each provider uses its own model.
Output of a call on 2026-10-07. The output is complete. The message has two topics, and OpenAI gave support the probability 0.80. The block sent the message to manual review. The probabilities can differ between calls and between model versions.
department: {"type":"choice","choice":"support","probabilities":{"billing":0.01,"support":0.8,"sales":0.19}}severity: {"type":"score","score":1.01,"probabilities":{"0":0.02,"1":0.95,"2":0.03}}wants_refund: {"type":"boolean","probability":0}model: gpt-6-lunausage: {"input_tokens":473,"output_tokens":0,"total_tokens":473}provider_metadata: {"usage":{"input_tokens":473,"input_tokens_details":{"cached_tokens":0,"cache_write_tokens":0},"output_tokens":0,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":473},"confidence":{"department":0.7,"severity":0.93}}warnings: []route: manual review (best option support, probability 0.80)The call with the message I was charged twice for my order. Please refund the extra charge. gave a clear result. These are the department line and the last line of the output:
department: {"type":"choice","choice":"billing","probabilities":{"billing":1,"support":0,"sales":0}}route: billing (probability 1.00)Replace two lines of the block. Set the web credential uc_ai_typesafe.g_apex_web_credential := 'TYPESAFE_AI'; and pass p_provider => uc_ai.c_provider_typesafe.
Output of a call on 2026-10-07 with the same message. The output is complete. TypeSafe reports the version of the model that answered. The order of the keys in probabilities is the order that TypeSafe returned.
department: {"type":"choice","choice":"support","probabilities":{"support":0.98,"sales":0.02,"billing":0}}severity: {"type":"score","score":1.16,"probabilities":{"0":0,"1":0.84,"2":0.16}}wants_refund: {"type":"boolean","probability":0.03}model: jev-1.13.0usage: {"input_tokens":473,"output_tokens":71}provider_metadata: {"usage":{"input_tokens":473,"output_tokens":71},"confidence":{"department":0.97,"severity":0.76}}warnings: []route: support (probability 0.98)The two providers read the same message differently. OpenAI gave support 0.80, and TypeSafe gave it 0.98. Set the threshold for the provider and the model that you use.
Read a refused answer
Section titled “Read a refused answer”The example sends one question that has an answer and one question that has none. OpenAI answers the first question and refuses the second. The call does not raise an error.
declare l_questions json_object_t := json_object_t(q'[{ "department": { "type": "choice", "instructions": "Which team should handle this message?", "criteria": { "billing": "Payments and refunds.", "support": "Technical problems." } }, "smell": { "type": "choice", "instructions": "Pick the colour of the number seven's smell.", "criteria": { "red": "Red.", "blue": "Blue." } } }]'); l_result json_object_t;begin uc_ai_openai.g_apex_web_credential := 'OPENAI'; l_result := uc_ai.decide( p_state => 'I was charged twice for my order.' , p_questions => l_questions , p_provider => uc_ai.c_provider_openai ); sys.dbms_output.put_line('answers: ' || l_result.get_object('answers').to_string); sys.dbms_output.put_line('usage: ' || l_result.get_object('usage').to_string); sys.dbms_output.put_line('warnings: ' || l_result.get_array('warnings').to_string);end;/Output of a call on 2026-10-07. The output is complete:
answers: {"department":{"type":"choice","choice":"billing","probabilities":{"billing":1,"support":0}},"smell":{"type":"refused"}}usage: {"input_tokens":234,"output_tokens":0,"total_tokens":234}warnings: []The smell answer has no choice. A call to get_string('choice') on it returns null. Test get_string('type') = 'refused' before you read an answer. The model decides which questions OpenAI refuses. The question in this example has no meaning, and OpenAI refused it in the call on 2026-10-07.
Read an image
Section titled “Read an image”The example asks which fruit an image shows. uc_ai_test_utils.get_apple_png returns a PNG from the UC AI tests. Replace it with your own blob. OpenAI and TypeSafe read images. The apple image is 200 by 200 pixels, which is too small for TypeSafe (minimum 256).
declare l_files json_array_t := json_array_t(); l_questions json_object_t := json_object_t(q'[{ "fruit": { "type": "choice", "instructions": "Which fruit is in the picture?", "criteria": { "apple": "A red or green apple.", "banana": "A banana.", "pear": "A pear." } } }]'); l_result json_object_t;begin uc_ai_openai.g_apex_web_credential := 'OPENAI';
l_files.append(uc_ai_message_api.create_file_content( p_media_type => 'image/png' , p_data_blob => uc_ai_test_utils.get_apple_png , p_filename => 'fruit.png' ));
l_result := uc_ai.decide( p_state => 'Look at the attached picture.' , p_questions => l_questions , p_provider => uc_ai.c_provider_openai , p_files => l_files );
sys.dbms_output.put_line('answers: ' || l_result.get_object('answers').to_string); sys.dbms_output.put_line('usage: ' || l_result.get_object('usage').to_string);end;/Output of a call on 2026-10-07. The output is complete:
answers: {"fruit":{"type":"choice","choice":"apple","probabilities":{"apple":1,"banana":0,"pear":0}}}usage: {"input_tokens":180,"output_tokens":0,"total_tokens":180}Decision agents
Section titled “Decision agents”A decision agent runs uc_ai.decide as an agent. It keeps the questions, the provider, the model, and the credential in its configuration. The run writes an execution row with the token counts, and a workflow can use the answers in a condition. Use a decision agent when the decision is one step of a multi-agent process. See Decision agents.
Route a ticket with a workflow
Section titled “Route a ticket with a workflow”The example is a workflow that routes a customer ticket. A decision agent asks which team handles the ticket. A conditional workflow then runs the profile agent of that team. If OpenAI refuses the question, the workflow runs a profile agent that writes a note for a person.
The workflow has four steps:
- The decision agent
ticket_routeranswers the questiondepartment. - The profile agent
billing_replyruns if the answer isbilling. - The profile agent
support_replyruns if the answer issupport. - The profile agent
human_review_noteruns if OpenAI refused the question.
Run the blocks as a user that has the web credential OPENAI. The profiles and the decision agent name this credential in their model configuration. The blocks do not set a session global.
Create the decision agent
Section titled “Create the decision agent”The decision agent asks one question. Its state_template is the placeholder {message}. The workflow passes the customer message in this input parameter.
DECLARE l_id NUMBER; l_config CLOB;BEGIN l_config := q'[{ "provider": "openai", "model": "gpt-6-luna", "state_template": "{message}", "questions": { "department": { "type": "choice", "instructions": "Which team should handle this message?", "criteria": { "billing": "Payments, invoices, and refunds.", "support": "Product usage and technical problems." } } }, "model_config": { "g_apex_web_credential": "OPENAI" } }]';
l_id := uc_ai_agents_api.create_agent( p_code => 'ticket_router', p_description => 'Decides which team handles a ticket', p_agent_type => uc_ai_agents_api.c_type_decision, p_decision_config => l_config, p_status => uc_ai_agents_api.c_status_active );
COMMIT;END;/The workflow also needs the three profile agents billing_reply, support_reply, and human_review_note. Open the block below to see the code that creates them.
Create the profile agents
Each profile agent has a prompt profile. The three profiles have the same structure. They write a reply or a note from the customer message.
DECLARE l_id NUMBER;BEGIN l_id := uc_ai_prompt_profiles_api.create_prompt_profile( p_code => 'BILLING_REPLY_PROFILE', p_description => 'First reply to a billing ticket', p_system_prompt_template => 'You write the first reply of the billing team to a customer. Write at most three sentences. Do not promise a refund.', p_user_prompt_template => 'Customer message: {message}', p_provider => uc_ai.c_provider_openai, p_model => uc_ai_openai.c_model_gpt_5_6_luna, p_model_config_json => '{"g_apex_web_credential": "OPENAI"}', p_status => uc_ai_prompt_profiles_api.c_status_active ); l_id := uc_ai_prompt_profiles_api.create_prompt_profile( p_code => 'SUPPORT_REPLY_PROFILE', p_description => 'First reply to a support ticket', p_system_prompt_template => 'You write the first reply of the support team to a customer. Write at most three sentences. Ask for the app version if it is missing.', p_user_prompt_template => 'Customer message: {message}', p_provider => uc_ai.c_provider_openai, p_model => uc_ai_openai.c_model_gpt_5_6_luna, p_model_config_json => '{"g_apex_web_credential": "OPENAI"}', p_status => uc_ai_prompt_profiles_api.c_status_active ); l_id := uc_ai_prompt_profiles_api.create_prompt_profile( p_code => 'HUMAN_REVIEW_NOTE_PROFILE', p_description => 'Note for a person who reviews a ticket', p_system_prompt_template => 'You write an internal note for a person who reviews a ticket by hand. State in one sentence that the automatic routing gave no answer. Then summarize the customer message in one sentence. Do not answer the customer.', p_user_prompt_template => 'Customer message: {message}', p_provider => uc_ai.c_provider_openai, p_model => uc_ai_openai.c_model_gpt_5_6_luna, p_model_config_json => '{"g_apex_web_credential": "OPENAI"}', p_status => uc_ai_prompt_profiles_api.c_status_active );
l_id := uc_ai_agents_api.create_agent( p_code => 'billing_reply', p_description => 'Writes the first reply to a billing ticket', p_agent_type => uc_ai_agents_api.c_type_profile, p_prompt_profile_code => 'BILLING_REPLY_PROFILE', p_status => uc_ai_agents_api.c_status_active ); l_id := uc_ai_agents_api.create_agent( p_code => 'support_reply', p_description => 'Writes the first reply to a support ticket', p_agent_type => uc_ai_agents_api.c_type_profile, p_prompt_profile_code => 'SUPPORT_REPLY_PROFILE', p_status => uc_ai_agents_api.c_status_active ); l_id := uc_ai_agents_api.create_agent( p_code => 'human_review_note', p_description => 'Writes a note for a person who reviews a ticket', p_agent_type => uc_ai_agents_api.c_type_profile, p_prompt_profile_code => 'HUMAN_REVIEW_NOTE_PROFILE', p_status => uc_ai_agents_api.c_status_active );
COMMIT; -- agents must be committed before they can be executedEND;/Create and run the workflow
Section titled “Create and run the workflow”The workflow type conditional runs the steps in order. A step with a condition runs only if the condition is true. See Conditional workflow.
DECLARE l_def CLOB; l_id NUMBER; l_result json_object_t; l_keys json_key_list;BEGIN l_def := q'[{ "workflow_type": "conditional", "steps": [ { "agent_code": "ticket_router", "input_mapping": { "message": "{$.input.message}" }, "output_key": "route" }, { "agent_code": "billing_reply", "condition": "'{$.steps.route.answers.department.choice}' = 'billing'", "input_mapping": { "message": "{$.input.message}" }, "output_key": "billing_draft" }, { "agent_code": "support_reply", "condition": "'{$.steps.route.answers.department.choice}' = 'support'", "input_mapping": { "message": "{$.input.message}" }, "output_key": "support_draft" }, { "agent_code": "human_review_note", "condition": "'{$.steps.route.answers.department.type}' = 'refused'", "input_mapping": { "message": "{$.input.message}" }, "output_key": "review_note" } ] }]';
l_id := uc_ai_agents_api.create_agent( p_code => 'ticket_workflow', p_description => 'Routes a ticket and drafts the first reply', p_agent_type => uc_ai_agents_api.c_type_workflow, p_workflow_definition => l_def, p_status => uc_ai_agents_api.c_status_active ); COMMIT;
l_result := uc_ai_agents_api.execute_agent( p_agent_code => 'ticket_workflow', p_input_parameters => json_object_t('{"message": "I was charged twice for my order. Please refund the extra charge."}'), p_session_id => 'doc_decision_wf_billing' );
DBMS_OUTPUT.PUT_LINE('status: ' || l_result.get_string('status')); DBMS_OUTPUT.PUT_LINE('final_message: ' || l_result.get('final_message').to_string); l_keys := l_result.get_object('steps').get_keys; FOR i IN 1 .. l_keys.count LOOP DBMS_OUTPUT.PUT_LINE('step ' || l_keys(i) || ': ' || l_result.get_object('steps').get(l_keys(i)).to_string); END LOOP;END;/Output of a call on 2026-10-07. The output is complete. The text of the reply is different in each call:
status: completedfinal_message: "We’re sorry for the billing issue. Please provide your order number and the dates and amounts of both charges so we can investigate the duplicate payment and determine the appropriate resolution."step route: {"answers":{"department":{"type":"choice","choice":"billing","probabilities":{"billing":1,"support":0}}},"warnings":[],"model":"gpt-6-luna","provider_metadata":{"usage":{"input_tokens":135,"input_tokens_details":{"cached_tokens":0,"cache_write_tokens":0},"output_tokens":0,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":135},"confidence":{"department":1}}}step billing_draft: "We’re sorry for the billing issue. Please provide your order number and the dates and amounts of both charges so we can investigate the duplicate payment and determine the appropriate resolution."A step that does not run has no entry in steps. Here support_draft and review_note are missing. The final_message of the workflow is the output of the last step that ran.
A second run with the message The export button in your app does nothing since the last update. gave this output. The decision agent chose support, and the profile agent support_reply wrote the reply:
status: completedfinal_message: "Sorry you’re having trouble with the export button after the update. Could you tell us which app version you’re using and what device and operating system it’s running on? If possible, please also let us know whether anything happens at all when you tap Export (such as an error message or loading indicator)."step route: {"answers":{"department":{"type":"choice","choice":"support","probabilities":{"billing":0,"support":1}}},"warnings":[],"model":"gpt-6-luna","provider_metadata":{"usage":{"input_tokens":134,"input_tokens_details":{"cached_tokens":0,"cache_write_tokens":0},"output_tokens":0,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":134},"confidence":{"department":1}}}step support_draft: "Sorry you’re having trouble with the export button after the update. Could you tell us which app version you’re using and what device and operating system it’s running on? If possible, please also let us know whether anything happens at all when you tap Export (such as an error message or loading indicator)."Notes on this example:
- The fourth step reads
type, notchoice. A refused question has nochoice, and a condition with a path that does not resolve is false, also with!=. See Branch on a refused answer. In the two runs above, OpenAI refused no question, so the fourth step did not run. - To send low-confidence answers to a person, add a fifth step with a numeric condition, for example
{$.steps.route.provider_metadata.confidence.department} < 0.8. Use a separate step. If either path is missing, a condition withoris false. A refused question has noconfidence. - The tokens of the decision step count in the run. In the first run,
ticket_routerused 135 input tokens andbilling_replyused 52 input tokens and 102 output tokens. The session totals show 187 and 102. See Read the tokens.
To delete the example, call uc_ai_agents_api.purge_agent for each of the five agent codes. Commit, and delete the three prompt profiles with uc_ai_prompt_profiles_api.delete_prompt_profile. The profiles stay after a purge.