internal/website/blog/28.md
June 19, 2026 • Asim Aslam
Most agent demos are a chat box wired to one tool. Real systems aren't that — they're a handful of services, an agent that operates them, something that triggers the agent without a human typing, and a gate on the actions you don't want it taking on its own. Here's that, built end to end. The full code is in examples/support; it runs with no API key.
The scenario: a customer files a ticket. That should trigger an agent to look the customer up, set a priority, and reply — but it must not email anyone without passing an approval gate.
Start with plain services. The agent will discover their endpoints as tools automatically — you don't describe them twice.
type CustomerService struct{}
// Lookup returns the customer with the given email.
// @example {"email": "[email protected]"}
func (s *CustomerService) Lookup(ctx context.Context, req *LookupRequest, rsp *Customer) error {
// ...
}
A tickets service (with Update) and a notify service (with Send, the action we'll gate) round it out. Three ordinary Go Micro services — nothing AI-specific about them.
An agent is a service with a model inside. Give it the services it manages and it turns their endpoints into tools:
support := micro.NewAgent("support",
micro.AgentServices("customers", "tickets", "notify"),
micro.AgentPrompt("You are a support agent. For each ticket, look up the "+
"customer, set a priority, and reply. Escalate billing issues."),
)
That's the whole agent. It discovers customers.Lookup, tickets.Update, and notify.Send, and the model decides which to call.
A support agent that waits for someone to type into a chat box is useless. The trigger is a ticket, so a flow turns that event into the agent's work:
intake := micro.NewFlow("intake",
micro.FlowTrigger("events.ticket.created"),
micro.FlowAgent("support"),
micro.FlowPrompt("A new support ticket arrived: {{.Data}}. Handle it."),
)
Now a ticket.created event on the broker is enough to set the agent going — no human in the loop to start it. (When the event is the prompt is the idea in full.)
The agent can read and triage all it likes. The action you actually care about is the one that reaches a customer — sending the email. That goes through a gate:
micro.AgentApproveTool(func(tool string, input map[string]any) (bool, string) {
if strings.Contains(tool, "Send") {
// return false to hold it for a human or a policy
log.Printf("approval gate: emailing %v", input["to"])
}
return true, ""
})
Return false and the send is refused with a reason the model sees — that's your human-in-the-loop, your spend cap, your billing sign-off. The agent never gets to email a customer on its own unless you let it. (Agent guardrails covers the rest.)
> event: events.ticket.created {"id":"ticket-1","customer":"[email protected]",...}
[customers] looked up Alice (pro plan)
[tickets] ticket-1 → priority=high status=in_progress
▣ approval gate notify_NotifyService_Send([email protected]) — approved
[notify] 📨 [email protected]: "Hi Alice — thanks for reaching out..."
✓ ticket triaged and the customer was replied to — triggered by an event
With the mock model it follows a fixed triage so it runs anywhere. Point it at a real model — go run main.go -provider anthropic — and the agent reasons about the ticket itself, choosing what to look up and how to reply.
Count what's AI-specific: the prompt, and one model option. Everything else is services, a flow, the broker, and a guardrail — the things Go Micro has always done. The agent didn't need a framework of its own; it's a service that calls other services, triggered by an event, with a gate on the dangerous action.
That's the shape of a real agent system, and it's the same shape as a real service system. From here you'd make the gate enforce a real policy, add a knowledge-base service for the agent to search, or expose the agent over A2A so another team's agent can file tickets. The code is in examples/support — clone it and change one thing.