Custom functions
Writing the functions a graph can call — the SDK's two builders, the gRPC transport underneath, and the import rule that stops external modules dead.
The built-in functions cover agents, templating, HTTP calls and data access. Everything else is yours to write — a function is just a typed handler on a worker you run.
The builder you will use
type In struct {
Region string `json:"region"`
Limit int `json:"limit"`
}
type Out struct {
Summary string `json:"summary"`
}
fn := sdk.NewSimpleFunction[In, Out](
"summarise_incidents", // name
"1.0.0", // version
"Summarise open incidents for a region.",
).WithHandler(func(in In) (Out, error) {
// your code
return Out{Summary: "…"}, nil
})
server.RegisterFunction(fn) The input and output structs are the contract: their JSON field names become
the node’s input and output ports, and their Go types become the types the
validator checks. This is why dibbla functions get can print a schema you
never wrote by hand.
The advanced Function[In, Out] handler signature exposes types from the
SDK’s internal/ packages, and Go’s internal/ rule forbids importing those
from any module other than sdk-go itself. If you find a tutorial using it,
it was written from inside the SDK repository. NewSimpleFunction is the
external surface.
The transport
Workers talk to the platform over gRPC: connect, announce what you provide, then stay connected and serve dispatches. You never write gRPC yourself — the SDK does it — but two of its properties leak into your life:
- A function exists only while its worker is connected. Stop the process and its nodes stop resolving; the workflow does not fail at author time, it fails at run time with an unknown-function error.
- The listener is cluster-internal. Reaching it from a laptop means a port forward; from a deployed app it is direct.
$SERVER_NAME=my-worker $GRPC_SERVER_ADDRESS=127.0.0.1:50051 # never rely on the default $GRPC_USE_TLS=false # true against a real endpoint $SERVER_API_TOKEN=…
GRPC_SERVER_ADDRESS defaults to grpc.dibbla.com:443. A worker started
without it — in a test, in CI, on your laptop — registers against production
and begins serving real dispatches. Set it explicitly everywhere.
Making it callable
Once registered, the function appears in the catalogue and can be wired like any built-in:

In YAML it is addressed by the (server, function) pair:
- id: summarise_incidents
type: function
function: summarise_incidents
server: my-worker
inputs:
region: eu-north
limit: 25
outputs: [summary]
Functions as agent tools
A function can also be given to an agent as a tool, which lets the model decide when to call it. One rule prevents the mistake everyone makes once:
A node may be an agent’s tool or a data input to that agent — never
both. Wiring both closes a cycle (the implicit tool-connection edge plus your
data edge) and the run is rejected with CYCLE_DETECTED before it starts. To
inject data into an agent’s context, template it into the system message
instead, and keep that node out of the tools: list.
The full SDK surface — server options, job handlers, the logger API, OAuth on behalf of a user — is in the Go SDK reference.