Skip to main content

Agent SOAT Tools and Preset Parameters

This tutorial shows how to give an agent access to platform documents using builtin tools — and how to use preset parameters to lock a tool to a specific document ID so the model never has to guess it. You will create a public and a private note, a restricted user alice, three builtin tools (docs_list-documents, docs_get-document, and docs_update-document with the public document's ID preset), and an agent that uses them — then verify the agent updates the right document and that alice's IAM policy blocks the private one.

Prerequisites

  • SOAT running locally with Ollama. Follow the Quick Start guide.
  • An Ollama instance accessible at http://ollama:11434 with model qwen2.5:0.5b pulled (ollama pull qwen2.5:0.5b).
  • CLI, SDK, or curl available. The server is at http://localhost:5047.
export SOAT_BASE_URL=http://localhost:5047

Step 1 — Log in as admin

Admin is the built-in superuser role. It bypasses policy evaluation entirely. See IAM — Authentication for details on JWT tokens and the admin role.

soat login-user --username admin --password Admin1234!
soat configure # paste the token when prompted

Step 2 — Create a project

Every resource in SOAT lives inside a project. Create one to hold the agent, documents, and tools.

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

Step 3 — Create an Ollama AI provider

Set up a local AI provider backed by Ollama. This tutorial uses a local Ollama provider so it can run without external credentials. To connect xAI, OpenAI, Anthropic, or Amazon Bedrock instead, see Connect Third-Party LLMs.

PROVIDER_ID=$(soat create-ai-provider \
--project-id "$PROJECT_ID" \
--name "Ollama" \
--provider "ollama" \
--default-model "qwen2.5:0.5b" | jq -r '.id')
echo "Provider: $PROVIDER_ID"

Step 4 — Create documents

Create two documents: a public note the agent will update, and a private note it must not touch.

PUBLIC_DOC_ID=$(soat create-document \
--project-id "$PROJECT_ID" \
--title "Public Note" \
--content "Initial public content." \
--path "/notes/public/note.txt" | jq -r '.id')
echo "Public doc: $PUBLIC_DOC_ID"

PRIVATE_DOC_ID=$(soat create-document \
--project-id "$PROJECT_ID" \
--title "Private Note" \
--content "Confidential information." \
--path "/notes/private/note.txt" | jq -r '.id')
echo "Private doc: $PRIVATE_DOC_ID"

Step 5 — Create user alice with a restricted policy

Alice is allowed to run agent generations and access documents under /notes/public/*. She cannot read or modify documents at other paths. See Users, Policies, and IAM — SRNs for the full access-control model.

ALICE_ID=$(soat create-user --username alice-agent-soat-tools --password Alice1234! | jq -r '.id')
echo "Alice: $ALICE_ID"

POLICY_ID=$(soat create-policy \
--name "alice-agent-soat-tools-notes-policy" \
--document '{
"statement": [
{
"effect": "Allow",
"action": ["agents:CreateAgentGeneration"]
},
{
"effect": "Allow",
"action": ["documents:ListDocuments"]
},
{
"effect": "Allow",
"action": ["documents:GetDocument", "documents:UpdateDocument"],
"resource": ["srn:'"$PROJECT_ID"':document:/notes/public/*"]
}
]
}' | jq -r '.id')

soat attach-user-policies \
--user-id "$ALICE_ID" \
--policy-ids '["'"$POLICY_ID"'"]'

Step 6 — Create builtin tools

Create three tools. Notice the third tool — docs-write — has preset_parameters containing the public document's ID. The key uses the parameter's wire name — snake_case (document_id). The model will never see the document_id field; it will be injected automatically at call time.

# Tool 1 — list documents
LIST_TOOL_ID=$(soat create-tool \
--project-id "$PROJECT_ID" \
--name "docs" \
--type builtin \
--actions '["list-documents"]' | jq -r '.id')

# Tool 2 — read any document (model supplies document_id)
READ_TOOL_ID=$(soat create-tool \
--project-id "$PROJECT_ID" \
--name "docs" \
--type builtin \
--actions '["get-document"]' | jq -r '.id')

# Tool 3 — update the public document (document_id is preset)
WRITE_TOOL_ID=$(soat create-tool \
--project-id "$PROJECT_ID" \
--name "docs" \
--type builtin \
--actions '["update-document"]' \
--preset-parameters '{"document_id": "'"$PUBLIC_DOC_ID"'"}' | jq -r '.id')

echo "List: $LIST_TOOL_ID"
echo "Read: $READ_TOOL_ID"
echo "Write: $WRITE_TOOL_ID"

The three tool names the model will see at runtime are:

Tool nameActiondocument_id visible to model?
docs_list-documentslist documentsN/A
docs_get-documentread a documentyes — model supplies it
docs_update-documentupdate a documentno — injected from preset_parameters

Step 7 — Create the agent

Create the agent and attach all three tools. The agent's instructions guide the model to use its tools when answering requests.

AGENT_ID=$(soat create-agent \
--project-id "$PROJECT_ID" \
--ai-provider-id "$PROVIDER_ID" \
--name "Notes Agent" \
--instructions "You are a note-taking assistant. Use your tools to list, read, and update documents." \
--tool-bindings "[{\"tool_id\":\"$LIST_TOOL_ID\"},{\"tool_id\":\"$READ_TOOL_ID\"},{\"tool_id\":\"$WRITE_TOOL_ID\"}]" | jq -r '.id')
echo "Agent: $AGENT_ID"

Step 8 — Log in as alice and run a generation

Alice asks the agent to update the public note via a session. The agent will call docs_update-document without knowing the document ID — the server injects it from preset_parameters.

# Log in as alice
ALICE_TOKEN=$(soat login-user --username alice-agent-soat-tools --password Alice1234! | jq -r '.token')

# Run the generation
RESULT=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/agents/$AGENT_ID/generate?wait=true" \
-H "Authorization: Bearer $ALICE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"role": "user",
"content": "Please update the public note with the content: Updated by the agent."
}
]
}')

echo "$RESULT" | jq '.'

Reuse a tool result as the generation input

You can also start a generation with Agents message content of type tool_output. The server executes the referenced tool first, applies output_path, and feeds the extracted value into the model as the user message.

A generation's messages carries user and assistant turns only. To steer how the agent reads that input, set the agent's instructions — a role: "system" entry here is rejected with 400 SYSTEM_MESSAGE_NOT_ALLOWED.

TOOL_OUTPUT_RESULT=$(soat create-agent-generation --wait true \
--agent-id "$AGENT_ID" \
--messages '[
{
"role":"user",
"content": {
"type": "tool_output",
"tool_id": "'"$READ_TOOL_ID"'",
"action": "get-document",
"input": {"document_id": "'"$PUBLIC_DOC_ID"'"},
"output_path": ".content"
}
}
]')

echo "$TOOL_OUTPUT_RESULT" | jq '.'

Step 9 — Verify the update and permissions

Confirm the agent updated the public document and was blocked from accessing the private one. This demonstrates how IAM policies enforce path-based access at runtime.

Confirm the public document was updated

soat get-document --document-id "$PUBLIC_DOC_ID" | jq '.content'
# Expected: "Updated by the agent."

Confirm alice cannot read the private document

curl -s "$SOAT_BASE_URL/api/v1/documents/$PRIVATE_DOC_ID" \
-H "Authorization: Bearer $ALICE_TOKEN" | jq '.'
# Expected: 403 Forbidden

The private document is inaccessible. If you asked the agent to update the private note, it would receive a 403 when trying to call docs_get-document with the private document's ID, and would report back that it is not permitted.


Step 10 — Call a tool directly via REST

You can invoke any non-client tool directly without creating an agent or a generation — useful for testing tool configurations, building custom pipelines, or integrating tool execution into your own code.

The list-documents builtin tool you created in step 6 already has project_id preset, so no extra parameters are needed in the request body.

soat call-tool \
--tool-id "$LIST_TOOL_ID" \
--action "list-documents"

The server merges preset_parameters into the call before dispatching — alice's IAM policy is still enforced. For http tools, omit the action field and pass parameters in input. For mcp tools, set action to the MCP tool name.


What happened

  1. Tool creation with preset_parameters: When you created docs-write, you stored { "document_id": "<public doc id>" } alongside the tool. The server stripped document_id from the schema before registering the tool with the model (preset keys use the parameter name exactly as the tool declares it — snake_case, the same spelling everywhere on the wire).

  2. Model's view: The model saw docs_update-document accepting only content, title, path, metadata, and tags — no document_id in sight. This eliminates the risk of the model supplying a wrong or hallucinated ID.

  3. Execution: When the model called docs_update-document, the server merged the preset document_id back in before dispatching the PATCH /api/v1/documents/{document_id} request.

  4. Permission enforcement: The request ran under alice's JWT. The platform's document permission check verified that alice's policy allows documents:UpdateDocument for the path /notes/public/note.txt. The private document path falls outside /notes/public/*, so any attempt there returns 403.


Next steps

  • Add more actions to the tools (e.g., search-knowledge) for richer agent workflows.
  • Use step rules to force the agent to call a specific tool first.
  • Explore boundary policies to limit which actions agents can use at the agent level, independent of caller IAM policies.
  • Read the agents module reference for the full list of soat actions and configuration options.