This document explains how to configure and deploy a Rowan agent for your use case. All deployment-specific configuration lives in a single YAML file (agent.yaml by default). The binary never needs to be rebuilt to change an agent's behaviour, tools, or languages.
agent.yaml ──► internal/agentconfig ──► profile.Profile ──► dialog manager
(loads & validates) (internal type) (unchanged)
At startup, the agent reads the YAML file, validates it, and wires up HTTP webhook clients in place of any business logic. The dialog manager is generic — it never knows whether tools came from a YAML file or compiled Go code.
Point the agent at a profile file via the environment variable:
ROWAN_PROFILE_FILE=/path/to/agent.yamlDefault: ./agent.yaml (relative to the working directory).
agent:
name: "Rowan" # Agent name used in the system prompt
domain: "order enquiries" # Business domain, e.g. "insurance claims"
languages:
- code: "en-US" # BCP-47 language code
name: "English"
fallback: "..." # Spoken on error
farewell: "..." # Spoken when max turns reached
transfer: "..." # Spoken before human transfer
welcome_prompt: "..." # LLM prompt to generate the opening greeting
response_instruction: "Respond in English."
verification: # omit entirely to disable identity verification
tool:
name: "verify_identity"
description: "..."
parameters: { ... } # JSON Schema object
webhook:
url: "https://api.example.com/verify"
secret: "bearer-token" # optional
guideline: "Before calling any sensitive tool, ask for..."
tools:
- name: "query_order_status"
description: "..."
sensitive: true # requires identity verification before use
webhook:
url: "https://api.example.com/orders/{order_id}"
method: GET # GET POST PUT PATCH DELETE — default POST
secret: "bearer-token" # optional
parameters: { ... } # JSON Schema object
policy:
- "Return policy: Items may be returned within 30 days of delivery in original packaging."
- "Shipping: Standard delivery takes 3-5 business days."
custom_guidelines:
- "Always ask for the order ID before calling any order tool."Each tool has a webhook block that controls how the agent calls it. The method field selects the HTTP method (default POST). Path parameters in {braces} in the URL are substituted from the tool params before the request is sent.
webhook:
url: "https://api.example.com/orders/{order_id}"
method: GET # GET POST PUT PATCH DELETE — default POST
secret: "bearer-token" # optionalParams not consumed by URL path substitution are appended as query string parameters. No request body is sent.
GET https://api.example.com/orders/123456
Authorization: Bearer <secret>
Params not consumed by URL path substitution are sent as a JSON body with the standard tool envelope.
POST https://api.example.com/orders
Content-Type: application/json
Authorization: Bearer <secret>
{"tool": "create_order", "params": {"product_id": "ABC", "quantity": 2}}
Success (2xx):
{"result": "Order 123456 is shipped and arriving tomorrow."}Error (any non-2xx, or "error" field present):
{"error": "Order not found."}The result string is passed directly to the LLM, which reads it to the customer in natural language. Error text is also surfaced to the LLM so it can inform the customer and decide how to proceed.
There are no retries — if a webhook call fails the error is surfaced immediately. Retry and circuit-breaker logic belongs in the customer backend or their infrastructure.
Verification always uses POST regardless of any method field — sensitive identity data must not appear in URLs or query strings.
POST https://api.example.com/verify
Content-Type: application/json
{"tool": "verify_identity", "params": {"phone_last4": "1234"}}
Success:
{"ok": true}Failure:
{"ok": false, "error": "Phone number does not match our records."}Set mock: true on any webhook to skip the HTTP call entirely. Useful for demos and local testing with no customer backend running.
tools:
- name: "query_order_status"
...
webhook:
mock: true
mock_response: "Order is shipped and on its way."For verification, mock: true always returns success:
verification:
...
webhook:
mock: trueWhen switching from demo to production, replace mock: true (and mock_response) with url: and optionally secret:.
The agent enforces scope and accuracy at two layers: code and prompt.
| Behaviour | Where it is enforced |
|---|---|
| Unknown tool names are rejected | dispatchTool returns an error for any tool not in the profile |
| Sensitive tools blocked without verification | dispatchTool checks state.IdentityVerified before calling the tool |
| Max 5 tool calls per turn | maxToolRounds constant in the dialog manager |
These cannot be bypassed by the LLM regardless of what it generates.
The system prompt instructs the agent to:
- Only help with configured topics. The capabilities list tells the LLM what it can do; the scope restriction sentence immediately after tells it to politely redirect for anything else.
- Never answer account/order questions without calling a tool first. This prevents the LLM from guessing or inventing account details.
- Report tool results faithfully. The LLM is told to convey exactly what the tool returned and not add information from general knowledge.
- Only answer general questions from the
policyitems. The policy section is injected as explicit reference context; the LLM is told not to draw on general knowledge for topics not covered there.
These are instructions to the LLM — they are highly effective but not absolute. No prompt-based instruction can offer a 100% guarantee against hallucination. For truly sensitive data (account numbers, balances, PII), the correct defence is to ensure that information only arrives via tool results, never in the system prompt or conversation history where the LLM could repeat it out of context.
Use policy items for factual information the agent should be able to recite without calling a tool — return policy, shipping times, support hours, accepted payment methods, etc.
policy:
- "Return policy: Items may be returned within 30 days of delivery in original packaging."
- "Shipping: Standard delivery takes 3-5 business days. Express (1-2 days) is available at extra cost."
- "Support hours: Monday to Friday, 08:00–18:00 CET."Keep each item as a single, self-contained statement. The agent will use these to answer general questions and will decline to speculate on anything not listed.
policy vs custom_guidelines:
| Field | Purpose | Example |
|---|---|---|
policy |
Factual knowledge the agent may recite | "Return window is 30 days." |
custom_guidelines |
Behavioural rules for how the agent should act | "Always collect the order ID before calling any tool." |
- The LLM can still produce imprecise paraphrases of tool results. If exact wording matters (e.g., legal disclosures), review and constrain the tool result string itself in your backend rather than relying solely on the prompt instruction.
- Verification protects tool calls, not free-text responses. If a customer mentions account details in conversation, the LLM may echo them back. Avoid injecting sensitive data into the conversation history; keep it inside tool responses.
- Out-of-scope redirection depends on the LLM correctly classifying requests. Adding a
custom_guidelinesentry like"If the customer asks about topics not covered by your tools or policy, say 'I can only help with [domain]' and list what you can do"reinforces the system prompt instruction for edge cases.
The dialog manager supports up to five sequential tool calls within a single turn. This lets the LLM complete multi-step workflows — for example, verify identity, then query the database, then send a notification — without extra round trips from the customer.
Order is driven entirely by the LLM, so make the intended sequence obvious in tool descriptions:
tools:
- name: "fetch_account"
description: "Retrieve the customer's account details. Always call this before any account-modification tool."
sensitive: true
...
- name: "update_email"
description: "Update the email address on a verified account. Requires fetch_account to have been called first."
sensitive: true
...When the customer says "update my email", the LLM calls fetch_account, reads the result, then calls update_email — all in one turn.
You can call CRM APIs directly using the appropriate HTTP method and URL template. For APIs that don't match the agent's response envelope ({"result": "..."}) you still need a thin integration layer to translate the response. For those that do, or where you control the API, no middleware is needed.
tools:
- name: "get_contact"
description: "Retrieve the customer's contact record from the CRM using their verified phone number."
sensitive: true
webhook:
url: "https://integration.example.com/crm/contacts/{phone}"
method: GET
secret: "bearer-token"
parameters:
type: object
properties:
phone:
type: string
description: "Customer phone number, digits only."
required: [phone]
- name: "create_case"
description: "Create a support case in the CRM on behalf of the customer."
sensitive: true
webhook:
url: "https://integration.example.com/crm/cases"
method: POST
secret: "bearer-token"
parameters:
type: object
properties:
subject:
type: string
description: "One-line summary of the issue."
description:
type: string
description: "Full description as described by the customer."
required: [subject, description]get_contact resolves to GET /crm/contacts/0701234567 — the phone param is substituted into the path and nothing goes in the query string. create_case sends POST /crm/cases with a JSON body envelope.
Map each operation to the correct HTTP method:
| Agent tool | Method | URL template | What it does |
|---|---|---|---|
create_ticket |
POST | /tickets |
Creates a new issue |
get_ticket_status |
GET | /tickets/{ticket_id} |
Looks up an issue by ID |
add_comment |
POST | /tickets/{ticket_id}/comments |
Appends a comment |
close_ticket |
DELETE | /tickets/{ticket_id} |
Closes/deletes an issue |
Keep result strings short and voice-friendly — they will be read aloud on the voice WebSocket channel.
tools:
- name: "query_order"
description: "Look up the status of an order by order ID."
sensitive: true
webhook:
url: "https://erp.example.com/api/orders/{order_id}"
method: GET
secret: "erp-token"
parameters:
type: object
properties:
order_id:
type: string
description: "The order ID as stated by the customer."
required: [order_id]
- name: "request_return"
description: "Initiate a return for an order. Always call query_order first to confirm the order exists."
sensitive: true
webhook:
url: "https://erp.example.com/api/orders/{order_id}/returns"
method: POST
secret: "erp-token"
parameters:
type: object
properties:
order_id:
type: string
reason:
type: string
description: "Return reason as described by the customer."
required: [order_id, reason]query_order resolves to GET /api/orders/123456 — order_id is substituted into the path, no query string needed. request_return sends POST /api/orders/123456/returns with {"tool": "request_return", "params": {"reason": "..."}} — order_id is consumed by the path template, only reason remains in the body.
The secret field adds Authorization: Bearer <token> to every webhook request. Store the token in your infrastructure secrets manager and inject it at deploy time — never commit secrets to the YAML file.
For stronger security, your webhook endpoint can additionally validate the source IP against the agent's egress IP range, or require mutual TLS.
The agent surfaces webhook errors to the LLM, which then tells the customer. Return customer-readable messages — avoid internal stack traces or system codes:
{"error": "We could not find that order number. Please check the number and try again."}Use HTTP status codes correctly: 2xx for success, 4xx for user-correctable errors (wrong ID, missing field), 5xx for transient backend failures. The agent does not retry — add retry and circuit-breaker logic in your integration layer.
- Mock mode first — set
mock: trueon all webhooks while authoring the YAML. Verify the LLM invokes the right tools in the right order before touching any real backend. - Integration layer staging — point webhook URLs at a staging environment with real data.
- Production cut-over — swap URLs and secrets, restart the agent. No rebuild needed.
Use the test/ws/ WebSocket voice client and the POST /chat endpoint to exercise tool flows end-to-end.
- Copy
agent.yamlto a new file, e.g.insurance-customer.yaml. - Set
agent.nameandagent.domainto match the customer's brand and use case. - Define languages — list every language the agent should support. The first entry is the default.
- Define tools — one entry per business operation. Set
sensitive: trueif identity verification must be done first. - Define verification — describe the identity check tool the LLM will call. Set the webhook to the customer's backend. Omit the
verificationblock entirely if the deployment needs no identity check. - Point the agent at the new file:
ROWAN_PROFILE_FILE=/path/to/insurance-customer.yaml
- Restart the agent — no rebuild needed.
- Add a new entry to the
toolslist in the YAML file with aname,description,sensitiveflag,webhook, andparameters. - Implement the corresponding endpoint in the customer backend.
- Restart the agent.
- Delete the tool entry from the
toolslist. - Restart the agent.
- Replace the
verificationblock with the new tool definition and webhook URL. - Restart the agent.
- Add a new entry to the
languageslist. - Restart the agent. The system prompt,
switch_languagetool, and ASR language detection all update automatically.
- Update
agent.nameand/oragent.domain. - Restart the agent.
- Edit the relevant
description,guideline, orcustom_guidelinesfields. - Restart the agent. The new wording takes effect for all new sessions.
internal/profile/profile.go— the internal Profile struct. Only change this if you need new platform capabilities.internal/dialog/manager.go— the generic dialog engine. This reads from the profile; never edit it for customer changes.- Meta-tools (
end_call,transfer_to_human,switch_language) — these are platform-level and built into the dialog manager. Do not define them in the YAML file.
| File | Purpose | When to edit |
|---|---|---|
agent.yaml |
Default customer profile (demo) | When updating the demo profile |
<customer>.yaml |
Customer-specific profile | When onboarding or updating that customer |
internal/agentconfig/agentconfig.go |
YAML schema + loader + validation | Only to add new profile fields |
internal/agentconfig/webhook.go |
HTTP webhook client | Only to change the webhook protocol |
internal/profile/profile.go |
Internal Profile types | Only to add new platform capabilities |
internal/dialog/manager.go |
Generic dialog engine | Never for customer changes |