> ## Documentation Index
> Fetch the complete documentation index at: https://motiadev-add-real-system-tutorial-round-2.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Functions

> Author the functions a new worker contributes to an iii system.

## What "writing a function" means

A worker contributes capabilities to a iii system by registering functions. Each function has an
`id` of the form `service::name`, a handler that receives the payload and returns a result, and
optional JSON Schemas that describe the request and response shape.

For how callers invoke functions (`worker.trigger` / `iii trigger` / event-bound triggers), see
[Using iii / Triggers](../using-iii/triggers). This page is about the authoring new functions.

## Register a function

Inside the worker, register the function with the SDK. The `id` is what triggers will use as a
`function_id`.

<Note>
  Each type of trigger has its own expected argument structure for a given function. For example
  `cron` will call functions without arguments, while `http` will provide a standard http-style
  payload that includes `body`, `headers`, and other properties. For each worker visit their
  respective page at [workers.iii.dev](https://workers.iii.dev/) for their expected payload.
</Note>

<Tabs>
  <Tab title="Node / TypeScript">
    ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    import { registerWorker } from "iii-sdk";

    const url = process.env.III_URL;
    if (!url) throw new Error("III_URL must be set");
    const worker = registerWorker(url);

    worker.registerFunction("math::add", async (payload: { a: number; b: number }) => {
      return { c: payload.a + payload.b };
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    import os
    from iii import register_worker, InitOptions

    worker = register_worker(
        os.environ.get("III_URL"),
        InitOptions(worker_name="math-worker"),
    )

    def add_handler(payload: dict) -> dict:
        return {"c": payload["a"] + payload["b"]}

    worker.register_function("math::add", add_handler)
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    use iii_sdk::{InitOptions, RegisterFunction, register_worker};

    let url = std::env::var("III_URL").expect("III_URL must be set");
    let worker = register_worker(&url, InitOptions::default());

    worker.register_function(RegisterFunction::new("math::add", |input: AddInput| {
        Ok(serde_json::json!({ "c": input.a + input.b }))
    }));
    ```
  </Tab>
</Tabs>

## Attach request and response schemas

Attach JSON Schemas to the registration so the request and response shape are documented alongside
the function. The schemas are stored with the function and surface in the iii console and the
agent-readable skills.

<Note>
  Runtime validation is not yet supported. Attached schemas are metadata only; the engine does not
  enforce a specific schema, reject payloads, nor handler return values that don't match the
  schemas. Treat the schemas as contract documentation for function invocations, agents, and the
  console.
</Note>

<Tabs>
  <Tab title="Node / TypeScript">
    ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    import { registerWorker } from "iii-sdk";

    const url = process.env.III_URL;
    if (!url) throw new Error("III_URL must be set");
    const worker = registerWorker(url);

    worker.registerFunction(
      "math::add",
      async (payload) => ({ c: payload.a + payload.b }),
      {
        request_format: {
          type: "object",
          properties: { a: { type: "number" }, b: { type: "number" } },
          required: ["a", "b"],
        },
        response_format: {
          type: "object",
          properties: { c: { type: "number" } },
          required: ["c"],
        },
      },
    );
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    import os
    from iii import register_worker, InitOptions

    worker = register_worker(
        os.environ.get("III_URL"),
        InitOptions(worker_name="math-worker"),
    )

    worker.register_function(
        "math::add",
        add_handler,
        request_format={
            "type": "object",
            "properties": {"a": {"type": "number"}, "b": {"type": "number"}},
            "required": ["a", "b"],
        },
        response_format={
            "type": "object",
            "properties": {"c": {"type": "number"}},
            "required": ["c"],
        },
    )
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    use iii_sdk::{InitOptions, RegisterFunction, register_worker};
    use schemars::JsonSchema;
    use serde::Deserialize;

    #[derive(Deserialize, JsonSchema)]
    struct AddInput { a: f64, b: f64 }

    #[derive(serde::Serialize, JsonSchema)]
    struct AddOutput { c: f64 }

    let url = std::env::var("III_URL").expect("III_URL must be set");
    let worker = register_worker(&url, InitOptions::default());

    // Rust derives `request_format` and `response_format` from the closure's
    // input and output types via `schemars::JsonSchema`. No builder methods
    // are needed — annotate your request/response structs and the SDK
    // generates the schemas automatically.
    worker.register_function(RegisterFunction::new(
        "math::add",
        |input: AddInput| -> Result<AddOutput, String> {
            Ok(AddOutput { c: input.a + input.b })
        },
    ));
    ```
  </Tab>
</Tabs>

The schemas also feed the iii console and the agent-readable skills.

## HTTP-invokable functions

You can also register an external HTTP endpoint as a function. The engine makes the HTTP call
whenever the function is invoked, your worker only declares the endpoint.

This is useful for delegating work to your existing API Gateways, webhooks, serverless platforms
(Lambda, Azure Functions, Google Cloud Functions), or any third-party API you want to surface as a
regular iii function.

The function is then triggerable like any other: `worker.trigger`, `iii trigger`, and any bound
trigger type (queue, cron, state, http) all work without any other changes.

<Accordion title="Example: register an external webhook as `notifications::send`">
  <Tabs>
    <Tab title="Node / TypeScript">
      ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
      import { registerWorker } from "iii-sdk";

      const url = process.env.III_URL;
      if (!url) throw new Error("III_URL must be set");
      const worker = registerWorker(url);

      worker.registerFunction(
        "notifications::send",
        {
          url: "https://hooks.provider.example.com/notify",
          method: "POST",
          timeout_ms: 5000,
          headers: { "X-Service": "iii-worker" },
          auth: { type: "bearer", token_key: "PROVIDER_API_TOKEN" },
        },
        { description: "POST a notification to the provider webhook" },
      );
      ```
    </Tab>

    <Tab title="Python">
      ```python theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
      import os
      from iii import HttpInvocationConfig, InitOptions, register_worker
      from iii.iii_types import HttpAuthBearer

      worker = register_worker(
          os.environ.get("III_URL"),
          InitOptions(worker_name="notifications-worker"),
      )

      worker.register_function(
          "notifications::send",
          HttpInvocationConfig(
              url="https://hooks.provider.example.com/notify",
              method="POST",
              timeout_ms=5000,
              headers={"X-Service": "iii-worker"},
              auth=HttpAuthBearer(token_key="PROVIDER_API_TOKEN"),
          ),
          description="POST a notification to the provider webhook",
      )
      ```
    </Tab>

    <Tab title="Rust">
      ```rust theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
      use std::collections::HashMap;
      use iii_sdk::{
          HttpAuthConfig, HttpInvocationConfig, HttpMethod, InitOptions,
          RegisterFunctionMessage, register_worker,
      };

      let url = std::env::var("III_URL").expect("III_URL must be set");
      let worker = register_worker(&url, InitOptions::default());

      let mut headers = HashMap::new();
      headers.insert("X-Service".into(), "iii-worker".into());

      worker.register_function((
          RegisterFunctionMessage::with_id("notifications::send".into())
              .with_description("POST a notification to the provider webhook".into()),
          HttpInvocationConfig {
              url: "https://hooks.provider.example.com/notify".into(),
              method: HttpMethod::Post,
              timeout_ms: Some(5000),
              headers,
              auth: Some(HttpAuthConfig::Bearer {
                  token_key: "PROVIDER_API_TOKEN".into(),
              }),
          },
      ));
      ```
    </Tab>
  </Tabs>
</Accordion>

### `HttpInvocationConfig` fields

While a normal function takes an `id` and a `handler`, http invokeable functions take an `id` and a
`HttpInvocationConfig`.

| Field        | Type                                              | Default    | Description                                                                                             |
| ------------ | ------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------- |
| `url`        | `string`                                          | (required) | Endpoint the engine calls when the function is invoked.                                                 |
| `method`     | `"GET" \| "POST" \| "PUT" \| "PATCH" \| "DELETE"` | `"POST"`   | HTTP method.                                                                                            |
| `timeout_ms` | `number`                                          | `30000`    | Per-request timeout in milliseconds.                                                                    |
| `headers`    | `Record<string, string>`                          | `{}`       | Headers added to every invocation.                                                                      |
| `auth`       | `HttpAuthConfig`                                  | (none)     | An object with two fields: `bearer`, `hmac`, or `api_key` AND `token_key`, `secret_key`, or `value_key` |

<Note>
  Auth fields (`token_key`, `secret_key`, `value_key`) are specified as the **names of environment
  variables**, not the secrets themselves. The engine resolves them from its own process environment
  at registration time, so secrets stay on the engine host and never travel over the SDK WebSocket.
</Note>

### HTTP error handling

The engine sends the invocation payload as the JSON request body and treats any non-2xx response or
network error as an invocation failure that propagates back to the caller. HTTP-invoked functions
appear in [`engine::functions::list`](../using-iii/functions#engine-functions-engine) and are
discoverable through the console exactly like in-process handlers.

## Return values and errors

A function returns either a value (which the handler is responsible for shaping to match its
documented [response schema](#attach-request-and-response-schemas)) or an error. Errors raised
inside the handler are propagated to the caller as invocation errors with the worker's stack trace
attached: Node forwards `error.stack`, Python forwards `traceback.format_exc()`, and Rust forwards
the underlying error's stack trace. The engine doesn't swallow them. Use this distinction to express
expected failures (return a structured error value) versus unexpected ones (throw / raise / return
`Err`).

## Unregister a function

`registerFunction` returns a handle with an `unregister()` method that removes the function from the
engine at runtime. When the worker disconnects, all of its functions are removed automatically and
pending invocations error out.

<Tabs>
  <Tab title="Node / TypeScript">
    ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    const add = worker.registerFunction("math::add", async (payload) => {
      return { c: payload.a + payload.b };
    });

    add.unregister();
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    add = worker.register_function("math::add", add_handler)

    add.unregister()
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    let add = worker.register_function(RegisterFunction::new("math::add", |input: AddInput| {
        Ok(serde_json::json!({ "c": input.a + input.b }))
    }));

    add.unregister();
    ```
  </Tab>
</Tabs>
