Skip to content

Tools

Tools are Skein’s mechanism for LLM function calling and structured external integrations. A tool declaration defines a contract (input schema, output schema) and an implementation. At compile time, the compiler generates JSON Schema manifests that LLM providers can use for function-calling.

Tools are declared inside modules with the tool keyword:

module PaymentService {
tool CreateRefund {
description: "Issue a refund via the payment provider"
input {
customer_id: String @description("Stripe customer ID")
amount: Int @min(1) @max(100000)
}
output {
id: String
status: String
}
errors { RefundError }
implement {
match http.post("https://api.stripe.com/v1/refunds", customer_id) {
Ok(r) -> Ok({ id: r.body.id, status: r.body.status })
Err(e) -> Err(RefundError.from(e))
}
}
}
}
tool <Name> {
description: "<text>" -- optional
input { -- required: input fields
<field>: <Type> [annotations]
}
output { -- required: output fields
<field>: <Type>
}
errors { <Error>, ... } -- optional: error types
implement { -- required: implementation body
<expressions>
}
}

The implement body must evaluate to Result[output, error] — the runtime invokes it and matches on Ok/Err, so a bare value is a compile error (E0020). Ok({ ... }) payloads are checked field-by-field against the declared output { ... } shape.

Tool names can use dots for namespacing:

tool Stripe.CreateRefund { ... }
tool Stripe.GetBalance { ... }

The compiler automatically generates JSON Schema from tool input and output fields. This is available at runtime via the __tools__/0 metadata function.

Given:

tool CreateRefund {
input {
customer_id: String @description("Stripe customer ID")
amount: Int @min(1) @max(100000)
}
output { id: String, status: String }
implement { Ok({ id: "r1", status: "ok" }) }
}

The generated input_schema is:

{
"type": "object",
"properties": {
"customer_id": { "type": "string", "description": "Stripe customer ID" },
"amount": { "type": "integer", "minimum": 1, "maximum": 100000 }
},
"required": ["amount", "customer_id"]
}

Constraint annotations (@min, @max, @description, @one_of, @default) are included in the schema. Option[T] fields are excluded from required.

Every compiled module with tools exports __tools__/0:

mod.__tools__()
#=> [%{
# name: "CreateRefund",
# description: "Issue a refund via the payment provider",
# input: [%{name: "customer_id", type: "String"}, %{name: "amount", type: "Int"}],
# input_schema: %{"type" => "object", "properties" => %{...}, "required" => [...]},
# output: [%{name: "id", type: "String"}, %{name: "status", type: "String"}],
# output_schema: %{"type" => "object", "properties" => %{...}, "required" => [...]},
# impl: :__tool_impl_0__
# }]

The input_schema and output_schema fields are proper JSON Schema objects suitable for passing directly to LLM function-calling APIs. impl names the compiled entry point for the tool’s implement block.

Tools are the one cross-module seam in Skein: a tool.call in one module resolves to a tool declaration in another through the runtime tool registry. When skein build, skein test, or skein run loads a compiled module, every tool it declares is registered automatically — the declared schemas are kept for input validation and the implement block becomes the tool’s executable body. Registration is idempotent: reloading a module overwrites its previous entries.

From Elixir, the same wiring is available directly:

Skein.Runtime.Tool.register_module(mod)

Use the tool.* effect calls to interact with tools at runtime. All three return a Result, so handle it with match or unwrap with !, and tool.call takes its arguments as a map literal:

module AgentService {
capability tool.use(CreateRefund)
fn process_refund(customer: String) -> String {
match tool.call(CreateRefund, { customer_id: customer, amount: 500 }) {
Ok(_) -> "refund created"
Err(_) -> "refund failed"
}
}
fn list_available() -> String {
match tool.list() {
Ok(_) -> "tools listed"
Err(_) -> "listing failed"
}
}
fn get_schema() -> Map[String, String] {
tool.schema(CreateRefund)!
}
}
EffectDescriptionReturns
tool.call(ToolName, args)Execute a registered toolResult[Map, ToolError]
tool.list()List all registered toolsResult[List[ToolInfo], ToolError]
tool.schema(ToolName)Get a tool’s schemaResult[Map, ToolError]

Tool calls require a capability tool.use(ToolName) declaration. Without it, the analyzer produces error E0012.

Every tool.call is automatically traced with:

  • kind: :tool
  • method: :call
  • name: "ToolName"
  • duration_us and outcome