Skip to Content
How-To GuidesGoCall a Golem Tool from Go

Call a Golem Tool from Go

A Go agent calls a tool’s command with the command’s own argument struct: Call starts from the declared defaults, lets you set the rest, and returns the typed result.

Grant the Tool

The calling agent must be bound to the tool in golem.yaml:

tools: greeter: {} agents: AssistantAgent: tools: greeter: {}

A binding on a component applies to all of its agents (see golem-edit-manifest).

A Tool of the Same Component

Call the command declared with tool.DefineTool (see golem-define-tool-go):

greeting, err := greeter.Greet.Call(func(a *greeter.GreetArgs) { a.Name = "ada" a.Loud = true }) if nf, ok := greeter.ErrNotFound.Match(err); ok { return "no such person: " + nf.Name } if err != nil { return "", err }

fill may be nil when the defaults are enough. The call goes through the runtime like any other, so the tool’s middleware applies, and the handler’s principal is the one the calling agent’s invocation runs on behalf of.

A Tool of Another Component

Depend on the tool, and golem build generates a typed client for it:

components: example:caller: dir: caller templates: go dependencies: tools: - example:provider/greeter

The client is golem-temp/bridge-sdk/go/internal/greeter-tool-guest-client, a Go module named golem.local/bridge/greeter-tool-guest-client. Point the calling component’s go.mod at it:

require golem.local/bridge/greeter-tool-guest-client v0.0.0 replace golem.local/bridge/greeter-tool-guest-client => ../golem-temp/bridge-sdk/go/internal/greeter-tool-guest-client

It declares the tool with tool.DefineToolClient and each command the same way, so calls look identical:

import greeter "golem.local/bridge/greeter-tool-guest-client" greeting, err := greeter.Greet.Call(func(a *greeter.GreetArgs) { a.Name = "ada" })

Names follow the command path: Tool (with its identity type, e.g. GreeterTool), Root for the tool’s own body, Greet, StockShow for stock show, with <Command>Args structs, <Node>Globals structs embedded in them, and Err<Case> error cases (Err<Command><Case> when two commands give one case different payloads). Defaults a Go literal can spell are filled in; set any other field yourself.

An existing built-in tool or MCP import

Load golem-generating-tool-clients to select a built-in release or an MCP import. Declare it, add it to the Go component’s dependencies.tools, and bind it separately; golem build generates the Go client without an explicit bridge selection. Do not copy the tool’s schema into a tool.DefineTool of your own. For the built-in bash release 0.2.1:

tools: bash: release: account: builtin-tool-owner@golem.cloud name: bash version: "0.2.1" components: example:caller: dir: caller templates: go dependencies: tools: [bash] tools: bash: filesystemAccess: allowed

Point go.mod at golem.local/bridge/bash-tool-guest-client as above, then:

import bash "golem.local/bridge/bash-tool-guest-client" result, err := bash.Run.Call(func(a *bash.RunArgs) { a.Script = "printf 'hello\\n'" }) if err != nil { return golem.Err[string, string](err.Error()) } if result.ExitCode != 0 { return golem.Err[string, string](result.Stderr) } return golem.Ok[string, string](result.Stdout)

cwd and timeout keep their declared defaults ("" and 600) unless the call sets a.Cwd or a.Timeout. The script’s exit code is part of the result, not an error: check ExitCode when a nonzero exit should fail the method. For an MCP import, use the generated result type rather than assuming the call returns a string.

A Tool Without Its Definition

tool.DefineToolClient[T](name) declares a tool’s shape for calling only, with the same commands, specs and error cases a definition has, but no handlers — what a generated client contains, and what to write by hand for a subset of a tool:

type Search struct{} var ( Tool = tool.DefineToolClient[Search]("document-search") Query = Tool.Command[QueryArgs, QueryResult]("query", func(a *QueryArgs, s *tool.CommandSpec) { s.Positional(&a.Text) }) )

Calls go to the definition’s name. When a deployment registers the same tool under another name, target it with Bind and On:

archive := Tool.Bind("archive-search") res, err := Query.On(archive).Call(func(a *QueryArgs) { a.Text = "golem" })

Stdin, Stdout and Stderr

A field bound with Stdin is an io.Reader; set it to feed the command. A required one left nil is refused before anything is sent. A command declared with OutputCommand (it writes standard output, standard error or both) returns a running invocation instead of the result:

inv, err := tools.Upper.Call(func(a *tools.UpperArgs) { a.In = strings.NewReader("hello") }) if err != nil { return err } out := inv.Collect() // out.Result/out.Err, out.Stdout/out.StdoutErr, out.Stderr/out.StderrErr
  • inv.Stdout() and inv.Stderr() take an output (an io.ReadCloser) to read while the command runs; then inv.Wait() returns the result. Bytes on stderr do not mean the command failed.
  • Wait drains and discards every output not taken before it, so a command never stalls on output nobody reads; taking one afterwards reads tool.ErrOutputDrained. An output you took is yours: read it while waiting, or a command writing more than the stream buffers stalls.
  • Collect reads both outputs concurrently while awaiting the result. Each keeps its own outcome: Err for the result, StdoutErr and StderrErr for the outputs (a *tool.OutputError when the command failed one), so a failed output does not hide the result and a failed result does not hide what the outputs carried. Stdout/Stderr are nil for a command without that output.
  • inv.Cancel() asks the runtime to cancel the call. To stop reading only one output, Close it: the command sees that output closed by its consumer, and the call, its result and the other output go on.

Errors

A failed call returns a *tool.CallError (errors.As): its Kind separates the tool’s declared errors (tool.CallDeclaredError, matched with ErrX.Match) from rejected input, constraint violations, denials, cancellation and runtime failures. Message carries the detail.

Discovered Tools

A tool with no Go declaration is called through discovery: reflection.DiscoverTool returns a snapshot of the deployed tool, and the client bound from it validates and packs every call against it, with canonical JSON arguments and results.

greeter, found := reflection.DiscoverTool("greeter") if !found { return fmt.Errorf("the greeter tool is not available to this agent") } client := greeter.Bind() out, err := client.Call([]string{"greet"}, map[string]any{ "name": "ada", "loud": false, "times": 2, "title": nil, }) var ce *tool.CallError if errors.As(err, &ce) && ce.Kind == tool.CallDeclaredError { fmt.Println(ce.ErrorName, golem.Must(ce.Payload().JSON())) }
  • Supply every field of the command’s input record by its wire name: inherited globals, positionals, the tail (a list), options and flags. Nothing is filled in from declared defaults.
  • client.Start(path, args, stdin) runs a command that reads standard input or has outputs, and returns the running invocation with every output the command declares; client.Call discards them.
  • cmd.Input() is the input record of a callable command (cmd.Callable()) and cmd.Output() the result type, golem.None when there is none, as schema.Refs that pack, unpack and render JSON Schema; tool.Command(path), tool.Commands() and cmd.Errors() describe the rest. Arguments that do not match fail with *schema.ValidationError before anything is sent. A snapshot never refreshes itself.
  • reflection.BindTool(name) (no error; nothing is checked until a call) gives a dynamic client that forwards already packed golem.TypedValues unchecked, with the same Call and Start; its Start takes tool.Streams{Stdout: true, Stderr: true} to request outputs, which must match what the command declares.
GuideDescription
golem-define-tool-goDefining the tool being called
golem-agent-reflection-goDiscovering and calling agents the same way
golem-tools-middleware-goIntercepting tool calls