--- title: Workers | Handinger description: Create, update, and delete reusable Handinger worker templates. --- A **worker** is a reusable agent template. Its title, system instructions, output schema, attached files, MCP connections, and members live on the worker — every task inherits them. Most production integrations create a small number of workers up front and run thousands of tasks against them. This guide covers the worker lifecycle: POST create, PATCH update, and DELETE delete. See [tasks](/guides/tasks/index.md) for how to actually run them. All requests on this page require a valid API key. See [authentication](/guides/authentication/index.md) for setup. ## Create a worker There are two ways to specify what the worker should do: 1. **`prompt`** — a natural-language description. Handinger expands it into structured instructions and, when the request implies extraction, an output schema. Use this to bootstrap a worker quickly. 2. **`instructions`** — a fully-specified system prompt. Use this once you know exactly what behaviour you want. When you pass `instructions`, Handinger does not modify them. ### From a prompt - [ TypeScript](#tab-panel-32) - [ Python](#tab-panel-33) - [ Go](#tab-panel-34) - [ Ruby](#tab-panel-35) - [ CLI](#tab-panel-36) - [ cURL](#tab-panel-37) ``` import Handinger from '@ramensoft/handinger'; const client = new Handinger({ apiKey: process.env['HANDINGER_API_KEY'], // This is the default and can be omitted }); const workerTemplate = await client.workers.create({ prompt: 'A worker that fact-checks short claims and returns a verdict with citations.', }); console.log(workerTemplate.id); ``` ``` import os from handinger import Handinger client = Handinger( api_key=os.environ.get("HANDINGER_API_KEY"), # This is the default and can be omitted ) worker_template = client.workers.create( prompt="A worker that fact-checks short claims and returns a verdict with citations.", ) print(worker_template.id) ``` ``` package main import ( "context" "fmt" "github.com/ramensoft/handinger-go" "github.com/ramensoft/handinger-go/option" ) func main() { client := handinger.NewClient( option.WithAPIKey("My API Key"), ) workerTemplate, err := client.Workers.New(context.TODO(), handinger.WorkerNewParams{ CreateWorker: handinger.CreateWorkerParam{ }, }) if err != nil { panic(err.Error()) } fmt.Printf("%+v\n", workerTemplate.ID) } ``` ``` require "handinger" handinger = Handinger::Client.new(api_key: "My API Key") worker_template = handinger.workers.create puts(worker_template) ``` Terminal window ``` handinger workers create \ --api-key 'My API Key' ``` Terminal window ``` curl https://handinger.com/api/workers \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $HANDINGER_API_KEY" \ -d "{ \"instructions\": \"You are a brand voice analyzer. Read the input text and report whether it matches Acme's playful, plain-spoken house style. Quote specific phrases.\", \"outputSchema\": { \"type\": \"bar\", \"required\": \"bar\", \"properties\": \"bar\" }, \"prompt\": \"A worker that fact-checks short claims and returns a verdict with citations.\", \"summary\": \"Audits copy against the Acme brand voice guide.\", \"title\": \"Brand voice analyzer\", \"visibility\": \"public\" }" ``` ### With explicit instructions and an output schema Pass `instructions` instead of `prompt` to skip the AI expansion step. When you want deterministic structured output, also pass JSON Schema Draft-07 in `outputSchema` — every task response will then expose a typed `structuredOutput` object that has been validated against the schema. A minimal `outputSchema` looks like this: ``` { "type": "object", "required": ["verdict", "summary"], "properties": { "verdict": { "type": "string", "enum": ["supported", "debunked", "mixed", "unknown"] }, "summary": { "type": "string" }, "citations": { "type": "array", "items": { "type": "string" } } } } ``` ### Visibility and members - `public` (default) — every member of the organization can list and run the worker. - `private` — only invited members and the creator can see or run it. You can flip visibility at any time with [update](#update-a-worker). To invite a specific user to a private worker, use the worker members endpoint (`POST /api/workers/{workerId}/members`). ### Create-time defaults What gets filled in automatically? - `title` — when omitted, Handinger assigns a random dog-themed name. - `summary` — auto-generated from your `prompt` when you don’t provide one. - `avatar` — a unique dog avatar is picked per organization. - `instructions` — generated from `prompt` when you don’t pass them. The generated prompt is detailed and durable; you can read it back via [retrieve](#retrieve-a-worker). - `outputSchema` — generated when your `prompt` implies extraction. Pass `null` to keep responses free-form. ## Retrieve a worker Use `workers.retrieve(workerId)` to fetch the current configuration plus the messages of the most recent task. This is the same endpoint the dashboard uses to render a worker’s chat view, so the response shape is task-centric (`output`, `output_text`, `messages`). - [ TypeScript](#tab-panel-38) - [ Python](#tab-panel-39) - [ Go](#tab-panel-40) - [ Ruby](#tab-panel-41) - [ CLI](#tab-panel-42) - [ cURL](#tab-panel-43) ``` import Handinger from '@ramensoft/handinger'; const client = new Handinger({ apiKey: process.env['HANDINGER_API_KEY'], // This is the default and can be omitted }); const worker = await client.workers.retrieve('t_org_123_w_01HZY2ZJQ8G7K42W2D7WF6V4GM'); console.log(worker.id); ``` ``` import os from handinger import Handinger client = Handinger( api_key=os.environ.get("HANDINGER_API_KEY"), # This is the default and can be omitted ) worker = client.workers.retrieve( worker_id="t_org_123_w_01HZY2ZJQ8G7K42W2D7WF6V4GM", ) print(worker.id) ``` ``` package main import ( "context" "fmt" "github.com/ramensoft/handinger-go" "github.com/ramensoft/handinger-go/option" ) func main() { client := handinger.NewClient( option.WithAPIKey("My API Key"), ) worker, err := client.Workers.Get( context.TODO(), "t_org_123_w_01HZY2ZJQ8G7K42W2D7WF6V4GM", handinger.WorkerGetParams{ }, ) if err != nil { panic(err.Error()) } fmt.Printf("%+v\n", worker.ID) } ``` ``` require "handinger" handinger = Handinger::Client.new(api_key: "My API Key") worker = handinger.workers.retrieve("t_org_123_w_01HZY2ZJQ8G7K42W2D7WF6V4GM") puts(worker) ``` Terminal window ``` handinger workers retrieve \ --api-key 'My API Key' \ --worker-id t_org_123_w_01HZY2ZJQ8G7K42W2D7WF6V4GM ``` Terminal window ``` curl https://handinger.com/api/workers/$WORKER_ID \ -H "Authorization: Bearer $HANDINGER_API_KEY" ``` Need to read the persistent template (title, instructions, schema, visibility)? [Create](#create-a-worker) and [update](#update-a-worker) return that shape directly. To re-read it later, store the template fields alongside your business records or rely on webhooks. ## Update a worker `PATCH /api/workers/{workerId}` performs a partial update — only the fields you send are changed. Subsequent tasks immediately pick up the new instructions; tasks already running keep their original prompt. Updating a worker requires the **creator** role on that worker. - [ TypeScript](#tab-panel-50) - [ Python](#tab-panel-51) - [ Go](#tab-panel-52) - [ Ruby](#tab-panel-53) - [ CLI](#tab-panel-54) - [ cURL](#tab-panel-55) ``` import Handinger from '@ramensoft/handinger'; const client = new Handinger({ apiKey: process.env['HANDINGER_API_KEY'], // This is the default and can be omitted }); const workerTemplate = await client.workers.update('t_org_123_w_01HZY2ZJQ8G7K42W2D7WF6V4GM', { title: 'Claim verdict v2', }); console.log(workerTemplate.id); ``` ``` import os from handinger import Handinger client = Handinger( api_key=os.environ.get("HANDINGER_API_KEY"), # This is the default and can be omitted ) worker_template = client.workers.update( worker_id="t_org_123_w_01HZY2ZJQ8G7K42W2D7WF6V4GM", title="Claim verdict v2", ) print(worker_template.id) ``` ``` package main import ( "context" "fmt" "github.com/ramensoft/handinger-go" "github.com/ramensoft/handinger-go/option" ) func main() { client := handinger.NewClient( option.WithAPIKey("My API Key"), ) workerTemplate, err := client.Workers.Update( context.TODO(), "t_org_123_w_01HZY2ZJQ8G7K42W2D7WF6V4GM", handinger.WorkerUpdateParams{ UpdateWorker: handinger.UpdateWorkerParam{ }, }, ) if err != nil { panic(err.Error()) } fmt.Printf("%+v\n", workerTemplate.ID) } ``` ``` require "handinger" handinger = Handinger::Client.new(api_key: "My API Key") worker_template = handinger.workers.update("t_org_123_w_01HZY2ZJQ8G7K42W2D7WF6V4GM") puts(worker_template) ``` Terminal window ``` handinger workers update \ --api-key 'My API Key' \ --worker-id t_org_123_w_01HZY2ZJQ8G7K42W2D7WF6V4GM ``` Terminal window ``` curl https://handinger.com/api/workers/$WORKER_ID \ -X PATCH \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $HANDINGER_API_KEY" \ -d "{ \"instructions\": \"You are a brand voice analyzer. Read the input text and report whether it matches Acme's playful, plain-spoken house style. Quote specific phrases.\", \"outputSchema\": { \"type\": \"bar\", \"required\": \"bar\", \"properties\": \"bar\" }, \"summary\": \"Audits copy against the Acme brand voice guide.\", \"title\": \"Claim verdict v2\", \"visibility\": \"private\" }" ``` ### Clearing the output schema To return a worker to free-form text responses, pass `outputSchema: null` explicitly — this is the one field where `null` and “omitted” mean different things. ``` await handinger.workers.update('wrk_vk81XUHKHG-qr4', { outputSchema: null }); ``` Changing `outputSchema` invalidates downstream consumers that decode `structuredOutput`. Roll a new worker (and migrate readers) when the schema change is breaking. ## Delete a worker `DELETE /api/workers/{workerId}` permanently removes the worker template **and** all of its tasks, turns, files, schedules, and MCP connections. The operation is not reversible. Only the **creator** of the worker may delete it. The endpoint always returns `{ "deleted": true }` once the cascade completes. - [ TypeScript](#tab-panel-44) - [ Python](#tab-panel-45) - [ Go](#tab-panel-46) - [ Ruby](#tab-panel-47) - [ CLI](#tab-panel-48) - [ cURL](#tab-panel-49) ``` import Handinger from '@ramensoft/handinger'; const client = new Handinger({ apiKey: process.env['HANDINGER_API_KEY'], // This is the default and can be omitted }); const deleteWorkerResponse = await client.workers.delete('t_org_123_w_01HZY2ZJQ8G7K42W2D7WF6V4GM'); console.log(deleteWorkerResponse.deleted); ``` ``` import os from handinger import Handinger client = Handinger( api_key=os.environ.get("HANDINGER_API_KEY"), # This is the default and can be omitted ) delete_worker_response = client.workers.delete( "t_org_123_w_01HZY2ZJQ8G7K42W2D7WF6V4GM", ) print(delete_worker_response.deleted) ``` ``` package main import ( "context" "fmt" "github.com/ramensoft/handinger-go" "github.com/ramensoft/handinger-go/option" ) func main() { client := handinger.NewClient( option.WithAPIKey("My API Key"), ) deleteWorkerResponse, err := client.Workers.Delete(context.TODO(), "t_org_123_w_01HZY2ZJQ8G7K42W2D7WF6V4GM") if err != nil { panic(err.Error()) } fmt.Printf("%+v\n", deleteWorkerResponse.Deleted) } ``` ``` require "handinger" handinger = Handinger::Client.new(api_key: "My API Key") delete_worker_response = handinger.workers.delete("t_org_123_w_01HZY2ZJQ8G7K42W2D7WF6V4GM") puts(delete_worker_response) ``` Terminal window ``` handinger workers delete \ --api-key 'My API Key' \ --worker-id t_org_123_w_01HZY2ZJQ8G7K42W2D7WF6V4GM ``` Terminal window ``` curl https://handinger.com/api/workers/$WORKER_ID \ -X DELETE \ -H "Authorization: Bearer $HANDINGER_API_KEY" ``` If you only want to stop using a worker, set `visibility: 'private'` and stop inviting members instead of deleting it. The history stays intact for audit. ## Common pitfalls 403 Forbidden when updating or deleting Update and delete require the **creator** role. If you’re using an API key that was issued by a different user, transfer ownership in the dashboard first, or have the original creator run the call. The worker keeps generating with the old instructions In-flight tasks finish with the prompt they started with. Wait for them to complete (or cancel them via the dashboard) before sending a new task. Output schema validation fails The schema must be valid JSON Schema Draft-07. Top-level `type: "object"` is required, and unknown keywords are rejected at create/update time, not at task time. Use [json-schema.org’s validators](https://www.jsonschemavalidator.net/) to debug.