Skip to main content
Last updated on

Extending the Demo Agent

The demo agent ships with built-in scenarios like travel booking and banking, but you can add your own goals, tools, and integrations. This guide covers the extension points in the demo repo — how to define what your agent can do, wire up the tools it needs, and register everything so the system picks it up.

OpenBox automatically governs all tool calls regardless of type. You don't need any extra configuration to get governance coverage for new goals or tools.

Prerequisites

This guide assumes you've completed Run the Demo or the Temporal Integration Guide and have the demo running locally. See the Demo Architecture Reference for a full breakdown of signals, activities, and endpoints.

How Goals and Tools Work

A goal is a scenario configuration that tells the agent what it's trying to accomplish and which tools it can use. Each goal defines a system prompt, a list of available tools, and an example conversation that helps the LLM understand the expected interaction pattern.

Tools come in two types:

  • Native tools — Custom Python functions implemented directly in the codebase. Use these for business logic specific to your application.
  • MCP tools — External tools accessed via Model Context Protocol servers. Use these for third-party integrations (Stripe, databases, APIs) without writing custom code.

A goal declares which tools it needs — both native and MCP. The agent follows the goal's description to orchestrate tool calls in the right order. The workflow engine automatically detects whether a tool is native or MCP and routes it accordingly.

Project Structure

These are the key files involved when adding goals and tools:

PathPurpose
goals/Goal definitions, one file per category (e.g., hr.py, finance.py)
goals/__init__.pyAggregates all goal lists into a single registry
tools/Native tool implementations, one file per tool
tools/__init__.pyMaps tool names to handler functions via get_handler()
tools/tool_registry.pyTool definitions (name, description, arguments) for the LLM
models/tool_definitions.pyDataclass definitions for AgentGoal, ToolDefinition, ToolArgument, MCPServerDefinition
shared/mcp_config.pyPredefined MCP server configurations
workflows/workflow_helpers.pyRouting logic that distinguishes native tools from MCP tools

Adding a Goal

Define the Goal

Create a new file in goals/ (e.g., goals/support.py). Each goal is an AgentGoal instance with these fields:

FieldTypeDescription
idstrUnique identifier, must match the value used in AGENT_GOAL env var
category_tagstrCategory for grouping (e.g., "hr", "finance", "travel")
agent_namestrUser-facing name shown in the chat UI
agent_friendly_descriptionstrUser-facing description of what the agent does
toolsList[ToolDefinition]Native tools available to this goal
descriptionstrLLM-facing instructions listing all tools by name and purpose, in order
starter_promptstrInitial prompt given to the LLM to begin the scenario
example_conversation_historystrSample interaction showing the expected flow
mcp_server_definitionMCPServerDefinition(Optional) MCP server configuration for external tools

Here's the simplest real goal in the demo — checking PTO balance, which uses a single native tool:

goals/hr.py
from typing import List

import tools.tool_registry as tool_registry
from models.tool_definitions import AgentGoal

starter_prompt_generic = "Welcome me, give me a description of what you can do, then ask me for the details you need to do your job."

goal_hr_check_pto = AgentGoal(
id="goal_hr_check_pto",
category_tag="hr",
agent_name="Check PTO Amount",
agent_friendly_description="Check your available PTO.",
tools=[
tool_registry.current_pto_tool,
],
description="The user wants to check their paid time off (PTO) after today's date. To assist with that goal, help the user gather args for these tools in order: "
"1. CurrentPTO: Tell the user how much PTO they currently have ",
starter_prompt=starter_prompt_generic,
example_conversation_history="\n ".join(
[
"user: I'd like to check my time off amounts at the current time",
"agent: Sure! I can help you out with that. May I have your email address?",
"user: bob.johnson@emailzzz.com",
"agent: Great! I can tell you how much PTO you currently have accrued.",
"user_confirmed_tool_run: <user clicks confirm on CurrentPTO tool>",
"tool_result: { 'num_hours': 400, 'num_days': 50 }",
"agent: You have 400 hours, or 50 days, of PTO available.",
]
),
)

hr_goals: List[AgentGoal] = [goal_hr_check_pto]

Register the Goal

Import your goal list in goals/__init__.py and extend the registry:

goals/__init__.py
from goals.support import support_goals

goal_list.extend(support_goals)

Then set AGENT_GOAL in your .env file to the goal's id:

.env
AGENT_GOAL=goal_hr_check_pto

Restart the worker (make run-worker) to pick up the new value.

Adding Native Tools

Define the Tool

Add a ToolDefinition to tools/tool_registry.py. This tells the LLM what the tool does and what arguments it expects:

FieldTypeDescription
namestrTool name as referenced in goal descriptions
descriptionstrLLM-facing explanation of what the tool does
argumentsList[ToolArgument]Input arguments (can be empty [])

Each ToolArgument has:

FieldTypeDescription
namestrArgument name
typestrType hint (e.g., "string", "number", "ISO8601")
descriptionstrLLM-facing explanation of the argument
tools/tool_registry.py
from models.tool_definitions import ToolArgument, ToolDefinition

current_pto_tool = ToolDefinition(
name="CurrentPTO",
description="Find how much PTO a user currently has accrued. "
"Returns the number of hours and (calculated) number of days of PTO. ",
arguments=[
ToolArgument(
name="email",
type="string",
description="email address of user",
),
],
)

Implement the Tool

Create a file in tools/ with a function that accepts args: dict and returns a dict. The file name and function name should match the tool name (without the _tool suffix):

tools/hr/current_pto.py
import json
from pathlib import Path


def current_pto(args: dict) -> dict:
email = args.get("email")

file_path = (
Path(__file__).resolve().parent.parent / "data" / "employee_pto_data.json"
)
if not file_path.exists():
return {"error": "Data file not found."}

data = json.load(open(file_path))
employee_list = data["theCompany"]["employees"]

for employee in employee_list:
if employee["email"] == email:
num_hours = int(employee["currentPTOHrs"])
num_days = float(num_hours / 8)
return {
"num_hours": num_hours,
"num_days": num_days,
}

return_msg = "Employee not found with email address " + email
return {"error": return_msg}

The return dict should match the output format shown in the goal's example_conversation_history.

Register the Handler

Two registration steps are required:

1. Add to tools/__init__.py — import the function and add a case to get_handler():

tools/__init__.py
from .hr.current_pto import current_pto

def get_handler(tool_name: str):
if tool_name == "CurrentPTO":
return current_pto
# ... other tools ...
raise ValueError(f"Unknown tool: {tool_name}")

2. Add to workflows/workflow_helpers.py — the is_mcp_tool() function in this file determines whether a tool is native or MCP. Native tools are identified by successfully looking them up in get_handler(). As long as your tool is registered in tools/__init__.py, routing works automatically.

Adding MCP Tools

Using a Predefined Server

The demo includes predefined MCP server configurations in shared/mcp_config.py. To use one, pass it as the mcp_server_definition in your goal:

goals/stripe_mcp.py
from typing import List

from models.tool_definitions import AgentGoal
from shared.mcp_config import get_stripe_mcp_server_definition

starter_prompt_generic = "Welcome me, give me a description of what you can do, then ask me for the details you need to do your job."

goal_mcp_stripe = AgentGoal(
id="goal_mcp_stripe",
category_tag="mcp-integrations",
agent_name="Stripe MCP Agent",
agent_friendly_description="Manage Stripe operations via MCP",
tools=[], # Will be populated dynamically
mcp_server_definition=get_stripe_mcp_server_definition(included_tools=[]),
description="Help manage Stripe operations for customer and product data by using the customers.read and products.read tools.",
starter_prompt="Welcome! I can help you read Stripe customer and product information.",
example_conversation_history="\n ".join(
[
"agent: Welcome! I can help you read Stripe customer and product information. What would you like to do first?",
"user: what customers are there?",
"agent: I'll check for customers now.",
"user_confirmed_tool_run: <user clicks confirm on customers.read tool>",
'tool_result: { "customers": [{"id": "cus_abc", "name": "Customer A"}, {"id": "cus_xyz", "name": "Customer B"}] }',
"agent: I found two customers: Customer A and Customer B. Can I help with anything else?",
"user: what products exist?",
"agent: Let me get the list of products for you.",
"user_confirmed_tool_run: <user clicks confirm on products.read tool>",
'tool_result: { "products": [{"id": "prod_123", "name": "Gold Plan"}, {"id": "prod_456", "name": "Silver Plan"}] }',
"agent: I found two products: Gold Plan and Silver Plan.",
]
),
)

mcp_goals: List[AgentGoal] = [
goal_mcp_stripe,
]

Custom MCP Server

Define an MCPServerDefinition directly in your goal:

FieldTypeDescription
namestrIdentifier for the MCP server
commandstrCommand to start the server (e.g., "npx", "python")
argsList[str]Command-line arguments
envDict[str, str](Optional) Environment variables for the server process
connection_typestrConnection type, defaults to "stdio"
included_toolsList[str](Optional) Specific tools to use; omit to include all
goals/my_mcp_goal.py
import os
from models.tool_definitions import AgentGoal, MCPServerDefinition

goal_my_mcp = AgentGoal(
id="goal_my_mcp_integration",
category_tag="integrations",
agent_name="My Integration",
agent_friendly_description="Interact with my external service.",
tools=[],
description="Help the user with these tools: ...",
starter_prompt="Greet the user and help them with the integration.",
example_conversation_history="...",
mcp_server_definition=MCPServerDefinition(
name="my-mcp-server",
command="npx",
args=["-y", "@my-org/mcp-server", f"--api-key={os.getenv('MY_API_KEY')}"],
env=None,
included_tools=["list_items", "create_item"],
),
)

How MCP Tools Are Routed

MCP tools are loaded automatically when the workflow starts and converted to ToolDefinition objects. The is_mcp_tool() function in workflows/workflow_helpers.py distinguishes native tools from MCP tools by attempting a get_handler() lookup — if the lookup fails, the tool is routed to the MCP server. No additional wiring is needed.

Tool Confirmation Patterns

The demo supports three approaches for confirming tool execution before it runs:

ApproachHow It WorksBest For
UI confirmation boxUser clicks a confirm button before tool runs. Controlled by SHOW_CONFIRM env var.General demo use
Soft promptGoal description instructs the LLM to ask for confirmation in conversation (e.g., "Are you ready to proceed?").Low-risk informational actions
Hard confirmation argumentAdd a userConfirmation ToolArgument to the tool definition. The LLM must collect explicit user consent before calling the tool.Sensitive or write operations

For tools that take action or write data, use the hard confirmation pattern:

tools/tool_registry.py
book_pto_tool = ToolDefinition(
name="BookPTO",
description="Book PTO start and end date. Either 1) makes calendar item, or 2) sends calendar invite to self and boss? "
"Returns a success indicator. ",
arguments=[
ToolArgument(
name="start_date",
type="string",
description="Start date of proposed PTO, sent in the form yyyy-mm-dd",
),
ToolArgument(
name="end_date",
type="string",
description="End date of proposed PTO, sent in the form yyyy-mm-dd",
),
ToolArgument(
name="email",
type="string",
description="Email address of user, used to look up current PTO",
),
ToolArgument(
name="userConfirmation",
type="string",
description="Indication of user's desire to book PTO",
),
],
)

Checklist

Adding a Goal

  1. Create a goal file in goals/ (e.g., goals/support.py)
  2. Define the AgentGoal with all required fields
  3. Export a list variable (e.g., support_goals = [goal_support_ticket])
  4. Import and extend the goal list in goals/__init__.py
  5. Set AGENT_GOAL in .env to the goal's id

Adding Native Tools

  1. Define the ToolDefinition in tools/tool_registry.py
  2. Implement the tool function in tools/ (accepts args: dict, returns dict)
  3. Import and add the handler to get_handler() in tools/__init__.py
  4. Reference the tool in your goal's tools list and description

Adding MCP Tools

  1. Add mcp_server_definition to your goal (use shared/mcp_config.py for common servers or define a custom MCPServerDefinition)
  2. Set any required environment variables (API keys, etc.)
  3. List the MCP tools in your goal's description so the LLM knows about them
  4. If creating reusable MCP server configs, add them to shared/mcp_config.py

Next Steps