Add AI to Your Legacy System in 5 Minutes: A Baize Hands-On Tutorial (Spring Boot Example)

go dev.to

Got a backend system that's been running for years — piles of APIs, complex business logic, and the thought of adding AI feels like a massive undertaking? This tutorial shows you how to hook up an AI assistant to your existing system in 5 minutes using Baize — no refactoring, no core code changes, at most one new endpoint.

Baize is an open-source AI Agent runtime written in Go. Its core ideas: sidecar deployment, APIs-as-tools, and human-in-the-loop approval for critical operations. It connects to business systems over HTTP, so it doesn't matter what your backend is — Spring Boot, Django, Express, Gin, ThinkPHP — if it has APIs, it can connect. The AI understands user intent, calls the right tools automatically, and pauses for human confirmation on sensitive operations.

Language-agnostic, but we'll use Spring Boot as the main example. The principles are fully universal — for other frameworks, just swap the Controller code in Step 3 for your language of choice. The Baize config file stays exactly the same.


Prerequisites

To follow along, you'll need two things:

  1. A backend service (we'll use a Spring Boot ticket system as an example, but feel free to use your own project)
  2. The Baize binary (a single executable, zero dependencies)

You can clone the repo which includes a demo ticket system and Baize configs:

git clone https://github.com/rebornace/baize.git
cd baize
Enter fullscreen mode Exit fullscreen mode

The Spring Boot code examples in this tutorial use Java 17 + Spring Boot 3.x. Readers using other languages or frameworks can focus on the configuration and protocol sections, and map the code examples to their own stack.


Step 1: Download Baize and Start It with One Command

Baize is a single static binary written in Go — no runtime to install.

Option 1: Download the Binary (Recommended)

Grab the latest release for your platform from GitHub Releases, unzip it, and you're ready to go.

Option 2: Build from Source

If you have Go 1.25+ locally, you can build it yourself:

git clone https://github.com/rebornace/baize.git
cd baize
go build -o baize ./cmd/baize
Enter fullscreen mode Exit fullscreen mode

Start in Demo Mode

Baize comes with a demo mode that uses a Mock LLM (no API key needed) and a built-in demo ticket system — perfect for getting the full flow working first.

# Windows
.\baize demo

# macOS / Linux
./baize demo
Enter fullscreen mode Exit fullscreen mode

When you see output like this, it's running:

baize demo listening on :8080
mock ticket server listening on :18080
Enter fullscreen mode Exit fullscreen mode

Open your browser and go to http://localhost:8080/ui — you'll see Baize's web dashboard.

For production, use baize start with a config file and a real LLM API key. We'll cover that later.

No Config File? No Problem — Configure via the Web UI

Baize supports zero-config startup — just run baize start without any config file, then open the Web UI and set everything up interactively: models, connectors, tools, approval rules — all of it. Great if you prefer a graphical interface over editing YAML by hand.

# Windows PowerShell
$env:BAIZE_API_KEY = "your-api-key"
.\baize start

# macOS / Linux
export BAIZE_API_KEY="your-api-key"
./baize start
Enter fullscreen mode Exit fullscreen mode

Once it's running, go to http://localhost:8080/uiSettings in the sidebar. You can configure your LLM provider, add connectors, and manage tools directly from the page. Changes take effect immediately — no restart needed.

This tutorial uses YAML config files for clarity in the step-by-step flow. Both approaches produce the same result — pick whichever you prefer.


Step 2: Declare Tools in a Config File

The core of how Baize connects to business systems is the Connector. The most common approach is the OpenAPI Connector — if you have an OpenAPI/Swagger spec, Baize automatically turns every endpoint into a tool the AI can use.

2.1 Prepare the OpenAPI Spec

Let's say your Spring Boot ticket system has these endpoints:

Method Path Description operationId
GET /tickets List all tickets list_tickets
POST /tickets Create a ticket create_ticket
GET /tickets/{id} Get ticket details get_ticket
PATCH /tickets/{id} Update ticket status update_ticket_status

Here's the corresponding OpenAPI spec (ticket-api.yaml):

openapi: 3.0.3
info:
  title: Ticket System API
  version: 1.0.0
paths:
  /tickets:
    get:
      operationId: list_tickets
      summary: List all tickets
      responses:
        "200":
          description: List of tickets
    post:
      operationId: create_ticket
      summary: Create a ticket
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title]
              properties:
                title: { type: string, description: Ticket title }
                priority: { type: string, description: Priority level }
      responses:
        "201":
          description: Created successfully
  /tickets/{id}:
    get:
      operationId: get_ticket
      summary: Get ticket details
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Ticket details
    patch:
      operationId: update_ticket_status
      summary: Update ticket status
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              properties:
                status: { type: string, description: New status }
      responses:
        "200":
          description: Updated successfully
Enter fullscreen mode Exit fullscreen mode

If your Spring Boot project already has springdoc-openapi or Swagger integrated, you can export the JSON directly from /v3/api-docs — Baize understands both YAML and JSON.

2.2 Write the Baize Config File

Create baize-config.yaml:

listen: ":8080"
store:
  driver: sqlite
  sqlite_path: ./data/baize.db
ui:
  enabled: true

llm:
  provider: openai_compatible
  base_url: https://api.deepseek.com
  model: deepseek-chat
  api_key_env: BAIZE_API_KEY

agent:
  id: ticket-agent
  system: "Youareaticketassistant.Youcanonlyaccesstheticketsystemthroughtools."

connector:
  id: ticket-api
  type: openapi
  spec: ./ticket-api.yaml
  base_url: http://localhost:8081   # Your Spring Boot service URL
  require_approval:
    - create_ticket
    - update_ticket_status

run:
  max_steps: 16
  tool_timeout_sec: 60
Enter fullscreen mode Exit fullscreen mode

Key configuration points:

  • connector.type: openapi — Auto-discovers tools from the OpenAPI spec
  • connector.spec — Path to your OpenAPI spec file
  • connector.base_url — Your backend service URL; the AI calls this address when using tools
  • require_approval — Tools listed here require human confirmation before execution (recommended for all write operations)

2.3 Start with the Config File

# Set your LLM API Key (DeepSeek example)
# Windows PowerShell
$env:BAIZE_API_KEY = "your-api-key"

# macOS / Linux
export BAIZE_API_KEY="your-api-key"

# Start
./baize start -c baize-config.yaml
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:8080/ui → click "Tools" in the sidebar, and you'll see all 4 ticket endpoints listed as AI-usable tools.

At this step, you haven't changed a single line of your business code — Baize is just another caller hitting your APIs, no different from a frontend app.


Step 3: Add a Callback Endpoint for Full Control (Spring Boot Example)

The OpenAPI approach above works great when "the APIs already exist and the AI can call them directly." But sometimes you need more control:

  • You want to add custom logic before/after AI calls (audit logging, rate limiting, parameter validation)
  • The tool logic is complex and not suitable for exposing as a direct API
  • You don't want the AI touching internal services directly

This is where Execution Callback mode comes in: instead of calling your business APIs directly, Baize sends a message saying "which tool to call and with what arguments" to an endpoint you specify. You decide how to execute it. The protocol is dead simple — any language that can serve HTTP can implement it.

Below is a Spring Boot example; the same pattern applies to Django, Express, Gin, and any other framework.

3.1 Add an /execute Endpoint (Spring Boot Version)

Add one Controller with one method to your Spring Boot project:

package com.example.ticket.controller;

import com.example.ticket.service.TicketService;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/baize")
public class BaizeCallbackController {

    private final TicketService ticketService;

    public BaizeCallbackController(TicketService ticketService) {
        this.ticketService = ticketService;
    }

    @PostMapping("/execute")
    public Map<String, Object> execute(
            @RequestHeader("X-Baize-Protocol") String protocol,
            @RequestBody Map<String, Object> body) {

        // Protocol check
        if (!"v0".equals(protocol)) {
            return Map.of(
                "content", Map.of("error", "unsupported protocol"),
                "is_error", true
            );
        }

        String tool = (String) body.get("tool");
        @SuppressWarnings("unchecked")
        Map<String, Object> args = (Map<String, Object>) body.get("arguments");

        // Dispatch to your business logic based on tool name
        Object result = switch (tool) {
            case "list_tickets" -> ticketService.listTickets();
            case "create_ticket" -> ticketService.createTicket(
                (String) args.get("title"),
                (String) args.get("priority")
            );
            case "get_ticket" -> ticketService.getTicket((String) args.get("id"));
            case "update_ticket_status" -> ticketService.updateStatus(
                (String) args.get("id"),
                (String) args.get("status")
            );
            default -> null;
        };

        if (result == null) {
            return Map.of(
                "content", Map.of("error", "unknown tool: " + tool),
                "is_error", true
            );
        }

        return Map.of(
            "content", result,
            "is_error", false
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

That's it — one Controller, one method, one switch statement for dispatch. You're in full control of the execution logic. Add logging, add authorization checks, add caching — whatever you need.

3.2 Update Baize Config to Point to the Callback URL

Change the connector in baize-config.yaml to callback mode:

connector:
  id: ticket-api
  type: openapi
  spec: ./ticket-api.yaml
  base_url: ""                           # Can be empty in callback mode
  execution_callback_url: http://localhost:8081/baize/execute
  require_approval:
    - create_ticket
    - update_ticket_status
Enter fullscreen mode Exit fullscreen mode

The key addition is execution_callback_url — with this field set, Baize won't call base_url directly. Instead, it sends tool invocation requests to this callback address.

You still need the OpenAPI spec file because Baize uses it to discover what tools exist and what parameters they take — that's tool discovery. The actual tool execution is handled by your Spring Boot service.

After restarting Baize, the user experience looks the same, but the execution path is completely different — every tool call now goes through your /baize/execute endpoint.


Step 4: Send a Message and Watch the AI Call Tools

Once Baize is running, you can talk to the AI in two ways: via the Web UI or the HTTP API.

Option 1: Web UI (Recommended for Getting Started)

Open http://localhost:8080/ui and type in the chat box:

Show me all the tickets
Enter fullscreen mode Exit fullscreen mode

You'll see the AI's reasoning process step by step, including the full list_tickets tool call — both the request parameters and the returned result.

Try this next:

Create a ticket with title "Login page loads slowly" and high priority
Enter fullscreen mode Exit fullscreen mode

Because create_ticket is in the require_approval list, the AI will pause and wait for your confirmation. Click Approve on the approval card that pops up, and only then will it actually execute the create operation. This is HITL (Human-in-the-Loop) — sensitive operations are never executed blindly.

Option 2: HTTP API (Great for Integration)

Baize exposes a full REST control plane that you can call from scripts or other systems.

Start a new conversation run:

curl -X POST http://localhost:8080/v0/runs \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "ticket-agent",
    "messages": [
      {
        "role": "user",
        "content": "Show me all the tickets"
      }
    ]
  }'
Enter fullscreen mode Exit fullscreen mode

The response includes a run_id. Use it to check status and stream events:

# Get run result
curl http://localhost:8080/v0/runs/{run_id}

# Stream events (SSE)
curl http://localhost:8080/v0/runs/{run_id}/stream
Enter fullscreen mode Exit fullscreen mode

Going Further

Adding More Tools

Tools come from the OpenAPI spec. Adding a tool = adding an endpoint to the spec, then restarting Baize (or hot-loading through the settings page).

For example, if you want to add a "close ticket" tool, add this path to ticket-api.yaml:

  /tickets/{id}/close:
    post:
      operationId: close_ticket
      summary: Close a ticket
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Closed successfully
Enter fullscreen mode Exit fullscreen mode

Then add close_ticket to require_approval, and you're done.

Timeouts and Retries

Baize has a default timeout for each tool call, which you can adjust in the config:

run:
  max_steps: 16
  tool_timeout_sec: 120    # Timeout per tool call, default is 60 seconds
Enter fullscreen mode Exit fullscreen mode

If your business APIs are occasionally flaky, you can handle retries inside your Spring Boot callback endpoint — Spring Retry or Resilience4j both work great. Baize just "sends the request and waits for the result" — retry strategy is up to you.

Handling Login / Auth

If your system requires login before calling APIs, Baize has you covered. Configure auth.capture:

connector:
  id: ticket-api
  type: openapi
  spec: ./ticket-api.yaml
  execution_callback_url: http://localhost:8081/baize/execute
  auth:
    mode: passthrough
    capture:
      tool_name_glob: "*login*"
      token_json_paths: ["accessToken", "data.accessToken"]
      label_json_paths: ["email"]
      header_template: "Bearer{{token}}"
Enter fullscreen mode Exit fullscreen mode

When the AI detects that login is required, it calls the login tool first, automatically extracts the token from the response, and attaches it to subsequent calls. Login info is stored in the session identity and persists across conversations.


FAQ

Q: Does Baize have to run in Kubernetes?

No. Baize is a single binary — it runs on bare metal, VMs, Docker, and Kubernetes alike. Put it on the same internal network as your legacy system, or even deploy it on the public network and call back over VPN (security will be weaker, of course).

Q: What if my OpenAPI spec is outdated or doesn't cover all endpoints?

Two options:

  1. Use Execution Callback mode — the OpenAPI spec just declares "what tools exist," and you write the actual execution logic yourself
  2. Use the HTTP Plugin v0 protocol — write a small standalone service to add capabilities your original system doesn't have

Q: Could the AI call APIs randomly and mess up my data?

No. Two layers of protection:

  1. Human approval — tools in the require_approval list pause before execution and wait for someone to click approve in the dashboard
  2. Read-only first — it's good practice to start with read-only endpoints open and put all write operations behind approval, then gradually open things up as you gain confidence

Q: Which LLMs are supported?

Any model compatible with the OpenAI API format works — including OpenAI, DeepSeek, Anthropic (via compatible gateways), local Ollama, and more. Just change llm.base_url and llm.model in the config.

Q: Can it connect to messaging platforms like Slack or Discord?

Yes. Baize's channels are designed as an extensible capability. You can use the Webhook channel to adapt any platform. One assistant, one set of tools, one approval flow — reachable from multiple channels.

Q: What's the difference between demo mode and production mode?

  • Demo mode (baize demo): Uses Mock LLM, no API key needed, includes a built-in demo ticket system — great for trying it out quickly
  • Production mode (baize start): Connects to a real LLM, works with real business systems — full feature set

Wrapping Up

Let's recap the whole flow — really just four steps:

  1. Download Baize → single binary, zero dependencies
  2. Write an OpenAPI spec + configure the connector → APIs become tools automatically
  3. (Optional) Add an /execute endpoint → full control over execution logic
  4. Chat → the AI calls tools automatically, with human approval for sensitive operations

Baize's design philosophy is "sidecar deployment, clean removal" — it doesn't intrude on your business system, it just adds an intelligent caller. Turn it on when you want to try it, turn it off when you don't. Your legacy system stays untouched.

Open Source Info

Baize · MIT License · Go 1.25 · Zero C dependencies

GitHub: https://github.com/rebornace/baize

Core features: ReAct Agent · OpenAPI Connector · HTTP Plugin v0 · Execution Callback · MCP Bridge/Export · HITL (Human-in-the-Loop) · Built-in Web UI

Supported LLMs: OpenAI / DeepSeek / any OpenAI-compatible API

Stars, issues, and PRs are all welcome.

Source: dev.to

arrow_back Back to Tutorials