Home / Articles

From Prototype to Production With Agents CLI: An Expense Approval Agent

Scaffold, test, evaluate and deploy an ADK agent to Cloud Run using Agents CLI, with tracing, BigQuery logging, CI/CD and RAG extensions covered along the way.

Publisher: REACTAPP.TOP12 min read2799 words

This article is published in English.

Sources: Building AI Agents in 30 Minutes from Prototype to Production

Anyone who has tried to ship an AI agent lately knows the pattern. In development it behaves: it answers questions, calls the right tools, makes sensible choices. Then somebody asks when it can go live, and days disappear into Docker, authentication, observability and deployment pipelines.

The agent code took a few hours. The infrastructure around it is taking weeks.

What follows is a walkthrough of building and deploying a production-style agent with Agents CLI, an open-source toolkit meant to absorb that deployment work. The example is an expense approval agent complete with business rules, evaluations and a cloud deployment, which the toolkit's pitch says takes roughly half an hour end to end. Treat that figure as a best case on a prepared account.

The deployment gap

Building an agent with the Agent Development Kit (ADK), LangChain or LlamaIndex is easy enough. Getting it into production is where the work hides. A production agent typically needs all of the following.

Local development

  • the agent code, which is the interesting part
  • a local test setup
  • mock data or API stubs

Testing and evaluation

  • test case definitions
  • metrics and scoring rubrics
  • an LLM-as-judge configuration
  • a regression suite

Production infrastructure

  • a container image
  • cloud deployment config for something like Cloud Run or Kubernetes
  • authentication and authorisation
  • secret management
  • a CI/CD pipeline

Observability

  • request tracing
  • prompt and response logging
  • cost tracking and analytics
  • performance monitoring
  • error alerts

Each item means learning another tool, reading another set of docs and wiring together separate systems. A coding assistant can help with agent logic, but it struggles with deployment because it doesn't know your infrastructure conventions, which leads to rounds of debugging.

What Agents CLI is

Agents CLI is a toolkit that offers one interface to the whole agent development lifecycle on Google Cloud. It works in two modes.

  1. Agent mode. You install "skills" into a coding assistant such as Gemini CLI, Claude Code or Cursor. The skills teach the assistant deployment patterns so that it makes correct decisions.
  2. Manual mode. You run the CLI commands yourself. Each command works on its own, and no AI assistant is needed.

The central idea is that instead of an assistant guessing at deployment conventions, it gets machine-readable context about when to use each deployment target, how to structure evaluations and which observability patterns work best.

The example: an expense approval agent

The agent reviews employee expense reports and decides on approval according to company policy. It uses Gemini models through Vertex AI for reasoning. The requirements:

  • approve routine claims below $50 automatically
  • catch duplicate submissions
  • raise a flag on odd patterns like weekend charges or unfamiliar vendors
  • escalate uncertain cases to human reviewers
  • one hard safety rule: never auto-approve anything over $50

Prerequisites

Make sure these are installed:

# Required tools
Python 3.11+
uv package manager
Node.js (for skills installation)
Google Cloud SDK (gcloud)

You also need a Google Cloud project with billing switched on, which you can create in the Google Cloud console.

Install the toolkit:

uvx google-agents-cli setup

Then authenticate with Google Cloud, set your project and enable the required APIs:

# Authenticate with Google Cloud
gcloud auth application-default login
# Set your project ID
gcloud config set project YOUR_PROJECT_ID
# Enable required APIs
gcloud services enable aiplatform.googleapis.com
gcloud services enable run.googleapis.com
gcloud services enable artifactregistry.googleapis.com

That configures Vertex AI access for the agent, which will call Gemini models through the Vertex AI API.

Step 1: scaffold the project

Rather than building directories and config files by hand, let the CLI generate the full project layout.

With a coding assistant such as Gemini CLI, Claude or Cursor, a prompt is enough:

Build an expense approval agent using agents-cli.
Auto-approve under $50, flag duplicates, escalate suspicious cases.
Start with a prototype.

The assistant reads the installed skills and produces a project with the right structure. The manual route is three commands:

agents-cli create expense-agent --prototype --yes
cd expense-agent
agents-cli install

The --prototype flag matters. It generates only the agent code and test structure and leaves out deployment pieces such as Docker, Terraform and CI/CD, which come later. The generated layout looks like this:

expense-agent/
├── app/
│   ├── agent.py          # Agent definition
│   └── app_utils/        # Telemetry helpers
├── tests/
│   ├── eval/
│   │   ├── evalsets/     # Test cases for LLM-as-judge
│   │   └── eval_config.json
│   ├── integration/
│   └── unit/
├── pyproject.toml        # Dependencies managed by uv
└── GEMINI.md             # Guidance for coding agents

Step 2: write the business logic

Next, implement the approval logic in app/agent.py. In ADK, as in most agent frameworks, tools are plain Python functions. The docstring is critical, because the model reads it to decide when and how to call each tool.

Tool 1: check expense history.

import json
from adk.models import Gemini
from adk.agents import Agent

def check_expense_history(
    employee_id: str,
    vendor: str,
    date: str,
    amount: float
) -> str:
    """Check if this expense was already submitted.
    Args:
        employee_id: Employee identifier
        vendor: Vendor name
        date: Expense date (YYYY-MM-DD)
        amount: Expense amount in USD
    Returns:
        JSON string with duplicate check results
    """
    # In production, this would query your expense database
    # Using mock data for demonstration
    mock_history = [
        {"date": "2026-04-15", "vendor": "Coffee Shop", "amount": 8.50},
        {"date": "2026-04-15", "vendor": "Uber", "amount": 24.00},
    ]
    for past in mock_history:
        if (past["vendor"].lower() == vendor.lower() and
            past["date"] == date and
            abs(past["amount"] - amount) < 0.01):
            return json.dumps({
                "is_duplicate": True,
                "message": f"Duplicate found: ${amount} at {vendor} on {date}"
            })
    return json.dumps({
        "is_duplicate": False,
        "message": "No duplicate found"
    })

Tool 2: get company policy.

def get_company_policy(category: str) -> str:
    """Retrieve expense policy for a given category.Args:
        category: Expense category (meals, transport, lodging, etc.)
    Returns:
        JSON string with policy rules
    """
    policies = {
        "meals": {"auto_approve_threshold": 50.00},
        "transport": {"auto_approve_threshold": 50.00},
        "lodging": {"auto_approve_threshold": 0.00},  # Always escalate
        "default": {"auto_approve_threshold": 25.00}
    }
    policy = policies.get(category.lower(), policies["default"])
    return json.dumps(policy)

Tool 3: escalate to a human.

def escalate_to_human(
    expense_id: str,
    reason: str,
    context: str
) -> str:
    """Route expense to human approval queue.Args:
        expense_id: Unique expense identifier
        reason: Why this needs human review
        context: Additional context for reviewer
    Returns:
        Confirmation message
    """
    # In production, this creates a ticket or notification
    print(f"\n🚨 ESCALATED: {expense_id}")
    print(f"Reason: {reason}\n")
    return json.dumps({
        "status": "escalated",
        "ticket_id": f"REVIEW-{expense_id}"
    })

Then the tools are wired into an agent with clear instructions and safety constraints:

root_agent = Agent(
    name="expense_approval_agent",
    model=Gemini(model="gemini-3.1-pro-preview"),  # Using Vertex AI
    instruction="""You are an expense approval agent. Review expenses and make
    approval decisions based on company policy.
    Process for every expense:
    1. Call get_company_policy() to check the category threshold
    2. Call check_expense_history() to verify no duplicate exists
    3. Make a decision: AUTO_APPROVE, ESCALATE, or REJECT
    Auto-approve only if ALL of these conditions are true:
    - Amount is under the threshold for that category
    - No duplicate found in expense history
    - Expense is from a business day (Monday-Friday)
    - Vendor appears legitimate
    Escalate to human if ANY of these conditions are true:
    - Amount is over the category threshold
    - Duplicate submission detected
    - Weekend expense without clear business justification
    - Uncertain about vendor legitimacy
    - Category is "lodging" (always requires human review)
    Safety rules (NEVER violate these):
    - NEVER auto-approve any expense over $50
    - NEVER approve without checking for duplicates first
    - ALWAYS provide clear reasoning for your decision
    - When in doubt, ESCALATE (false escalation is better than false approval)
    Response format:
    Decision: [AUTO_APPROVE | ESCALATE | REJECT]
    Reasoning: [Clear explanation of why you made this decision]
    Tools called: [Which tools you used]
    """,
    tools=[check_expense_history, get_company_policy, escalate_to_human],
)

Look closely at the safety rules in the instruction. Even if policy would allow approving a meal up to $50, a hard limit applies: the agent never auto-approves anything over $50 without human review. That guards against expensive mistakes.

Step 3: test locally

Start the ADK playground:

agents-cli playground

It opens a web interface on localhost port 8080. Three cases cover the basics.

Routine approval.

Review this expense:
Employee: emp-123
Category: meals
Vendor: Coffee Shop
Date: 2026-04-18
Amount: $12.50

Expected behaviour: the agent calls get_company_policy(), sees a $50 threshold, calls check_expense_history(), finds no duplicate and approves.

Over the threshold.

Review this expense:
Employee: emp-456
Category: meals
Vendor: Restaurant
Date: 2026-04-18
Amount: $85.00

Expected behaviour: the agent escalates because the amount exceeds $50.

Duplicate submission.

Review this expense:
Employee: emp-789
Category: transport
Vendor: Uber
Date: 2026-04-15
Amount: $24.00

Expected behaviour: the agent finds the duplicate in the mock history and escalates.

Manual testing proves the happy path. Production readiness needs something more systematic.

Step 4: structured evaluations

Most developers skip this step, and it's a big reason agents break in production. You need test cases with clear pass or fail criteria. The question isn't "does this feel right?" but "does this meet policy?"

Edit tests/eval/evalsets/basic.evalset.json:

{
  "eval_set_id": "expense_approval_eval",
  "name": "Expense Policy Validation",
  "eval_cases": [
    {
      "eval_id": "routine_approval",
      "conversation": [
        {
          "user_content": {
            "parts": [{
              "text": "Review: emp-101, meals, Coffee Shop, 2026-04-18, $15.00"
            }]
          }
        }
      ],
      "session_input": {
        "app_name": "app",
        "user_id": "eval_user",
        "state": {}
      },
      "expected_tool_calls": ["get_company_policy", "check_expense_history"],
      "expected_output_keywords": ["approve", "approved"]
    },
    {
      "eval_id": "escalate_over_threshold",
      "conversation": [
        {
          "user_content": {
            "parts": [{
              "text": "Review: emp-102, meals, Restaurant, 2026-04-18, $85.00"
            }]
          }
        }
      ],
      "session_input": {
        "app_name": "app",
        "user_id": "eval_user",
        "state": {}
      },
      "expected_tool_calls": ["get_company_policy", "escalate_to_human"],
      "expected_output_keywords": ["escalate", "human review"]
    },
    {
      "eval_id": "reject_duplicate",
      "conversation": [
        {
          "user_content": {
            "parts": [{
              "text": "Review: emp-103, transport, Uber, 2026-04-15, $24.00"
            }]
          }
        }
      ],
      "session_input": {
        "app_name": "app",
        "user_id": "eval_user",
        "state": {}
      },
      "expected_tool_calls": ["check_expense_history"],
      "expected_output_keywords": ["duplicate", "escalate"]
    }
  ]
}

Run the evaluation:

agents-cli eval run

Sample output:

Running expense_approval_eval...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✓ routine_approval       policy_compliance: 1.0  safety: 1.0  tool_usage: 1.0
✓ escalate_over_threshold   policy_compliance: 1.0  safety: 1.0  tool_usage: 1.0
✓ reject_duplicate       policy_compliance: 0.9  safety: 1.0  tool_usage: 0.9
Overall Pass Rate: 100% (3/3 cases)
Average Scores: policy_compliance=0.97, safety=1.0, tool_usage=0.97

Scoring is done by an LLM acting as judge, working from the rubrics in eval_config.json. When a case fails, you iterate on the agent's instruction or tool logic until every case passes.

That cycle is the eval-fix loop, the key to reliable production agents. Without evals you're hoping the agent works, and with them you know.

Step 5: deploy

The agent works and passes every evaluation, so it's time to ship. First add the deployment infrastructure:

agents-cli scaffold enhance --deployment-target cloud_run

That generates a Dockerfile, Cloud Run deployment configuration and Terraform files. Then set the project and deploy:

gcloud config set project YOUR_PROJECT_ID
agents-cli deploy

Expect output along these lines:

Building container image...
Pushing to us-central1-docker.pkg.dev/your-project/agents/expense-agent...
Deploying to Cloud Run...
✓ Deployment successful!
Service URL: https://expense-agent-xyz123-uc.a.run.app
Logs: https://console.cloud.google.com/run/detail/.../logs

The claimed total from agents-cli create to a live service is around 30 minutes, though your own first run will depend on account setup and permissions.

Step 6: observability

Cloud Trace is enabled automatically, with no configuration. To try the deployed agent:

agents-cli run "Review expense: emp-501, transport, Uber, 2026-04-18, $15.00"
agents-cli run "Review expense: emp-502, meals, Restaurant, 2026-04-18, $95.00"
agents-cli run "Review expense: emp-503, lodging, Hilton, 2026-04-18, $200.00"

Then open Cloud Trace to see:

  • full request traces with a latency breakdown
  • the duration of each LLM call
  • tool execution times
  • an end-to-end timeline for each request

For debugging and analysis, switch on content logging:

agents-cli infra single-project --project YOUR_PROJECT_ID

That provisions a service account for logging, a Cloud Storage bucket for prompt and response storage, a BigQuery dataset for analytics and an update so the agent logs every conversation. You can then query production behaviour in BigQuery:

SELECT
  timestamp,
  JSON_EXTRACT_SCALAR(request_data, '$.user_content.parts[0].text') as request,
  JSON_EXTRACT_SCALAR(response_data, '$.decision') as decision,
  latency_ms
FROM `your-project.agent_analytics.conversations`
WHERE service_name = 'expense-agent'
ORDER BY timestamp DESC
LIMIT 10;

This is vital for debugging. When someone asks why the agent escalated a $30 meal, you query BigQuery and see exactly what it decided and why.

What sets the toolkit apart

Four design choices stand out.

1. Skills as machine-readable context. Agents CLI provides more than commands. It provides skills that teach your coding assistant to use them properly. Running agents-cli setup installs seven of them:

  • google-agents-cli-workflow for the development lifecycle
  • google-agents-cli-adk-code for ADK Python API patterns
  • google-agents-cli-scaffold for project scaffolding
  • google-agents-cli-eval for evaluation methodology
  • google-agents-cli-deploy for deployment targets
  • google-agents-cli-publish for Gemini Enterprise registration
  • google-agents-cli-observability for tracing and logging

The assistant consults them when relevant. Instead of guessing, it knows when to choose Agent Runtime, Cloud Run or GKE, and it knows which LLM-as-judge patterns work, so there's no trial and error with eval metrics.

2. Evaluation by default. Every scaffolded project includes evaluation infrastructure from the start: a tests/eval/evalsets/ folder for cases, eval_config.json for scoring rubrics and an LLM-as-judge setup. That reflects reality, since agents without evals rarely reach production, and making evaluation the default path means agents get tested before deployment.

3. Observability by default. Cloud Trace is enabled automatically on every deployed agent. Debugging a production agent without traces is close to impossible. Content logging and BigQuery analytics are opt-in through agents-cli infra single-project, but basic tracing is mandatory.

4. Infrastructure handled for you. Agent logic is interesting. Docker configs, auth middleware and Terraform files are not. The CLI takes on the infrastructure so you can spend time making the agent smart.

Going further

The expense agent meets the stated requirements, but a real deployment calls for a few more pieces.

Live data. Swap the mocked history for a query against your expense database:

import psycopg2

def check_expense_history(employee_id, vendor, date, amount):
    conn = psycopg2.connect(DATABASE_URL)
    cursor = conn.cursor()
    cursor.execute("""
        SELECT * FROM expenses
        WHERE employee_id = %s
        AND vendor = %s
        AND date = %s
        AND amount = %s
    """, (employee_id, vendor, date, amount))
    # Process results...

CI/CD. Generate a GitHub Actions workflow for staging and production:

agents-cli infra cicd --runner github-actions

It creates a multi-environment pipeline in which commits to main deploy to staging and tagged releases deploy to production.

Session storage. Add Cloud SQL for persistent sessions:

agents-cli scaffold enhance --session-type cloud_sql

Multi-agent systems. The adk_a2a template lets specialist agents coordinate, which suits complex workflows like incident response or code migrations:

agents-cli create coordinator-agent --agent-type adk_a2a

Retrieval-augmented generation. Scaffold an agent with vector search:

agents-cli create docs-agent \
  --agent-type agentic_rag \
  --datastore agent_platform_vector_search

That generates data ingestion code, vector search integration and retrieval logic.

The techniques in brief

Each technique here targets one stage of the lifecycle: scaffolding with a prototype flag for quick starts, tools as documented Python functions, hard safety constraints in the instruction, local playground testing, structured evaluations with a fix loop, generated infrastructure for deployment, and tracing plus content logging for visibility. Together they form a path from prototype to production.

The takeaway

The deployment gap is real. Many agent projects stall not because the agent is poor but because shipping it takes too much infrastructure work.

Agents CLI doesn't solve everything. You still need well-designed prompts, suitable tools and thorough testing. What it does remove is the infrastructure busywork that keeps agents trapped in development environments. The goal is for agent workflows to feel like running create-react-app and not like assembling infrastructure by hand.