Skip to main content

Goal

Register functions with request_format and response_format (JSON Schema objects) so callers—including AI agents and the CLI know exactly what JSON to send and what to expect back.

Why this matters

When you register a function, the iii engine stores metadata for discovery. The built-in engine::functions::list function returns every registered function with its request_format and response_format.
  • AI and automation — Agents can read that list and construct valid payload objects without guessing field names or types.
  • CLI and scripts — The same schemas document how to build --payload for iii trigger.
  • Contracts — Formats are a machine-readable contract between your implementation and anything that triggers it.
If you omit formats, discovery still lists your function, but request_format / response_format may be null, leaving callers without a structured contract.

Discover functions (and their formats) from the CLI

List everything registered on a running engine:
iii trigger --function-id=engine::functions::list
The result includes a functions array. Each entry has function_id, description, request_format, response_format, and optional metadata. For example, a state helper might look like this (trimmed):
{
  "functions": [
    {
      "function_id": "state::get",
      "description": "Get a value from state",
      "request_format": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "key": { "type": "string" },
          "scope": { "type": "string" }
        },
        "required": ["key", "scope"],
        "type": "object"
      },
      "response_format": null
    }
  ]
}
Use request_format to shape the JSON you pass to trigger() or iii trigger --payload='...'. Use response_format when you need to document or validate the return shape (when present).

Register formats in your SDK

Define schemas in the way that fits each language, then register the function. The engine stores the JSON Schema (or schema-compatible object) you send.
Use Zod (or any library that produces JSON Schema) and pass the schema objects on registration. The Node SDK does not infer formats from TypeScript types—you set request_format and response_format explicitly.
hello-world-zod.ts
import { z } from 'zod'
import { registerWorker } from 'iii-sdk'

const iii = registerWorker(process.env.III_URL ?? 'ws://localhost:49134')

const inputSchema = z.object({
  scope: z.string(),
  key: z.string(),
})

const outputSchema = z.object({
  value: z.string(),
})

async function helloWorld(input: z.infer<typeof inputSchema>): Promise<z.infer<typeof outputSchema>> {
  return { value: `${input.scope}::${input.key}` }
}

iii.registerFunction(
  {
    id: 'example::hello-world',
    description: 'Resolve a scoped key to a single string value',
    request_format: z.toJSONSchema(inputSchema),
    response_format: z.toJSONSchema(outputSchema),
  },
  helloWorld,
)
Keep one Zod schema per direction (input / output) so types and JSON Schema stay in sync via z.infer.

Auto-extraction vs explicit schemas

SDKRequest/response formats
NodeSet explicitly (e.g. z.toJSONSchema(...)). No automatic extraction from TypeScript types.
PythonWith register_function("id", handler), formats are auto-extracted from annotations (Pydantic models yield full JSON Schema).
RustRegisterFunction::new / new_async derives schemas from types that implement JsonSchema (via schemars).
The engine does not validate every payload against your schema before invoking your handler—that depends on your runtime. Treat request_format / response_format as the documented contract for callers; still validate inside the handler when correctness is critical.

Next steps

Trigger Functions from the CLI

Invoke any function with iii trigger and a JSON payload

Use Functions & Triggers

Register functions, triggers, and cross-language invocation patterns