Skip to main content

Route Work with a Decider

An orchestration that asks a decider whether to pay a refund now or send it to a person, then takes the branch the decider chose. The decider is answered by a pipeline tool that reads the customer's tags and applies two rules: a refund of 50 or less is paid, and so is any refund for a gold customer. No AI provider is required.

Prerequisites​

export SOAT_BASE_URL=http://localhost:5047

Step 1 — Log in as admin​

See Users for authentication.

ADMIN_TOKEN=$(soat login-user --username admin --password Admin1234! | jq -r '.token')
export SOAT_TOKEN=$ADMIN_TOKEN

Step 2 — Create a project​

Every resource below belongs to one project.

PROJECT_ID=$(soat create-project --name "refund-desk" | jq -r '.id')
echo "Project: $PROJECT_ID"

Step 3 — Tag two customers​

Each customer is an actor whose tier tag is what the rules read. Replacing an actor's tags sends the whole tag map as the body (PUT /api/v1/actors/{actor_id}/tags).

RITA_ID=$(soat create-actor --project-id "$PROJECT_ID" --name Rita | jq -r '.id')
GIL_ID=$(soat create-actor --project-id "$PROJECT_ID" --name Gil | jq -r '.id')

soat replace-actor-tags --actor-id "$RITA_ID" --tags '{"tier": "standard"}'
soat replace-actor-tags --actor-id "$GIL_ID" --tags '{"tier": "gold"}'
echo "Rita: $RITA_ID Gil: $GIL_ID"

Step 4 — Write the refund rules as a pipeline​

A decider sends its tool { input, questions } and reads back { answers: [...] }, one answer per question (The tool backend). The pipeline sees that body as its own input, so the decision's input is at input.input. It reads the tags of the customer named there with a builtin get-actor-tags step, then builds the answer to the route question in its output: approve when the amount is 50 or less or the tier tag is gold, review otherwise.

RULES_ID=$(soat create-tool \
--project-id "$PROJECT_ID" \
--name refund-rules \
--type pipeline \
--description "Pays small refunds and any refund for a gold customer" \
--pipeline '{
"steps": [
{
"id": "customer",
"tool": {"name": "read-customer", "type": "builtin", "actions": ["get-actor-tags"]},
"action": "get-actor-tags",
"input": {"actor_id": {"var": "input.input.customer_id"}}
}
],
"output": {
"answers": [
{
"name": "route",
"choice": {
"if": [
{"or": [
{"<=": [{"var": "input.input.amount"}, 50]},
{"==": [{"var": "steps.customer.tier"}, "gold"]}
]},
"approve",
"review"
]
}
}
]
}
}' | jq -r '.id')
echo "Rules: $RULES_ID"

The get-actor-tags step runs as whoever requested the decision, so it reads only the actors that caller may read.


Step 5 — Create the decider​

The decider holds one choice question and names the pipeline as its backend with tool_id.

DECIDER_ID=$(soat create-decider \
--project-id "$PROJECT_ID" \
--name refund-route \
--tool-id "$RULES_ID" \
--questions '[
{
"type": "choice",
"name": "route",
"instructions": "Pay this refund now, or send it to a person?",
"choices": [
{"value": "approve", "description": "Pay the refund now."},
{"value": "review", "description": "A person checks it first."}
]
}
]' | jq -r '.id')
echo "Decider: $DECIDER_ID"

Step 6 — Let the orchestration request decisions​

An orchestration reaches the decider through a builtin tool that exposes the create-decision action.

DECIDE_TOOL_ID=$(soat create-tool \
--project-id "$PROJECT_ID" \
--name request-decision \
--type builtin \
--description "Requests a decision from a decider" \
--actions '["create-decision"]' | jq -r '.id')
echo "Decide tool: $DECIDE_TOOL_ID"

Step 7 — Create the orchestration​

NodeTypePurpose
decidetoolRequests a decision on the refund in the run input
routeconditionEmits the decider's route answer as its label
paytransformRuns on the approve branch
escalatetransformRuns on the review branch

A tool call waits for its decision, so decide's artifact is the settled decision, and route reads the answer by question name at nodes.decide.answers_by_name.route.choice (The nodes.<id> namespace).

ORCH_ID=$(soat create-orchestration \
--project-id "$PROJECT_ID" \
--name refund-desk \
--nodes '[
{
"id": "decide",
"type": "tool",
"tool_id": "'"$DECIDE_TOOL_ID"'",
"operation_id": "create-decision",
"input_mapping": {
"decider_id": "'"$DECIDER_ID"'",
"input": {"customer_id": {"var": "input.customer_id"}, "amount": {"var": "input.amount"}}
}
},
{"id": "route", "type": "condition", "expression": {"var": "nodes.decide.answers_by_name.route.choice"}},
{
"id": "pay",
"type": "transform",
"expression": {"cat": ["PAID ", {"var": "input.amount"}]},
"state_mapping": {"state.result": {"var": "output.result"}}
},
{
"id": "escalate",
"type": "transform",
"expression": {"cat": ["REVIEW ", {"var": "input.amount"}]},
"state_mapping": {"state.result": {"var": "output.result"}}
}
]' \
--edges '[
{"from": "decide", "to": "route"},
{"from": "route", "to": "pay", "condition": "approve"},
{"from": "route", "to": "escalate", "condition": "review"}
]' | jq -r '.id')
echo "Orchestration: $ORCH_ID"

Step 8 — Route three refunds​

Rita's 30 is under the limit and is paid; her 400 is sent to review; Gil's 400 is paid because he is gold. Each run blocks until it settles (Start a run).

SMALL_RUN=$(soat start-orchestration-run --orchestration-id "$ORCH_ID" \
--input '{"customer_id": "'"$RITA_ID"'", "amount": 30}' --wait true)
LARGE_RUN=$(soat start-orchestration-run --orchestration-id "$ORCH_ID" \
--input '{"customer_id": "'"$RITA_ID"'", "amount": 400}' --wait true)
GOLD_RUN=$(soat start-orchestration-run --orchestration-id "$ORCH_ID" \
--input '{"customer_id": "'"$GIL_ID"'", "amount": 400}' --wait true)

SUMMARY='{status, route: .state.nodes.decide.answers_by_name.route.choice, result: .state.result}'
echo "$SMALL_RUN" | jq -c "$SUMMARY"
echo "$LARGE_RUN" | jq -c "$SUMMARY"
echo "$GOLD_RUN" | jq -c "$SUMMARY"

Expected output:

{"status":"completed","route":"approve","result":"PAID 30"}
{"status":"completed","route":"review","result":"REVIEW 400"}
{"status":"completed","route":"approve","result":"PAID 400"}

Step 9 — Read the decision behind a run​

Every run's decide artifact carries the decision's id; the decision is its own record, with the decider version that answered (Decision).

DECISION_ID=$(echo "$LARGE_RUN" | jq -r '.state.nodes.decide.id')

soat get-decision --decision-id "$DECISION_ID" \
| jq '{status, decider_id, decider_version, answers}'

Expected output:

{
"status": "completed",
"decider_id": "dcd_…",
"decider_version": 1,
"answers": [{ "type": "choice", "name": "route", "choice": "review" }]
}

Next Steps​

  • Answer the same question with judgment instead of rules: point the decider at an agent with agent_id (Backends).
  • Put a person on the review branch: Approval Gate.