Field Report // NO. 013

How to build a headless worker agent (and when not to)

9 August 2026· 8 min read· Case № 013

Build a Hermes profile with no chat channel, no persona, no memory, and four tools, for jobs that genuinely need a fresh context. The exact config, the verifiable-handle rule, and the operating costs that only appear after you run it.

The agent that annoyed me most in the whole fleet was the one I never actually talked to. I had a system-admin profile that could uninstall software, clean up directories, and run bulk file work, but it shared a chat channel, a memory, and a persona with a profile that was supposed to be answering me. Every maintenance session mixed two jobs that should never have been in the same conversation. So I built a worker with no chat channel at all. No persona, no memory writes, four tools. Its whole personality is a return value.

Then I made the mistake this build eventually taught me not to: I ran it as the default shape of the fleet. Every operational task routed to the worker, every interactive profile stripped to orchestration. Within days I was writing the plan to tear that out. The build was fine. The mandate was the mistake. This article is the build with the lesson already inside, so you get to skip my detour: the headless worker is a capability you choose per job, not a shape every profile should copy.

Thesis

When you finish, you can decide whether a headless worker fits how you run things, and if it does, build one: a Hermes profile with no chat gateway, no persona, no memory writes, and only file, terminal, delegation, and code tools. You will also know the three operating costs that only show up after you run it, and the one rule that keeps its output checkable.

Why a headless worker at all

A profile in Hermes is a directory with its own config, environment, skills, memory, and sessions. By default it is a chat surface: gateway, persona, memory, broad toolset. That is exactly right for an assistant you talk to.

Bulk execution work has a different shape. Uninstalling a service, cleaning a cache directory, processing fifty files: that work does not need a personality, does not need memory, and does not need to be able to message you. It needs to do the job and hand back something you can check.

The failure I kept hitting was context mixing. The profile that manages my host was also a Telegram chat surface. When a long uninstall ran inside a chat session, the persona, the memory, and the chat history were all loaded into the same context that was supposed to be doing focused work. Every session carried baggage that had nothing to do with the job.

The fix is a profile designed to carry none of it. I created agentic-worker, a headless profile with no chat channels, no persona, no memory writes, and a toolset restricted to four tools. It exists to do one job in a clean context and report back. That is the whole product.

The build

Step 1 — Create the profile without the baggage

Normal profile creation bundles skills by default. A headless worker gets its procedure from the task context, not from an installed skill library, so create it empty:

hermes profile create agentic-worker --no-skills --no-alias --description "Headless bulk-work subagent. No chat, no persona. Spawned via delegate_task."

--no-skills opts out of the bundled-skill sync, so the profile starts with an empty skills directory. --no-alias skips the per-profile command wrapper; you are never going to run this profile interactively. --description is used by the kanban board, the shared task list profiles hand work through, to route tasks by role rather than by name. Worth setting even if you never use the board.

Step 2 — Make it headless

The worker must not be reachable by chat. Two settings do that, one CLI key and one YAML block:

hermes config set platforms '{}'

That empties the messaging gateway config, so the profile has no Telegram, no Slack, no channel at all. Nothing can message it and it cannot message you.

Second, in config.yaml:

memory:
  provider: mnemosyne
  memory_enabled: false

memory_enabled: false turns off profile-level auto-write. The worker can still read the memory tools, but it does not accumulate its own notes between sessions. A headless worker should stay clean between jobs: nothing carries over except what the caller explicitly puts in the task context. If you find a stable pattern the worker should remember, you write it to a skill, not to memory.

Step 3 — Restrict the toolset

This is the part that makes it safe. A headless worker gets a minimal surface, defined in config.yaml:

platform_toolsets:
  cli:
    - file
    - terminal
    - delegation
    - code_execution

Four tools. That is the whole surface.

  • file — read and write local files.
  • terminal — shell commands, git, package managers, network probes.
  • delegation — the fan-out tool, covered in step 5.
  • code_execution — Python for one-off data manipulation.

Everything else is disabled. The worker does not have web, vision, kanban, memory, chat, or its own skill editor. If it thinks it needs one of those, the right answer is to return that as a finding to the caller, not to try to enable it. The bare toolset is the design. A focused workhorse has no reason to browse, no reason to chat, and no reason to edit its own skills.

Step 4 — Set the delegation bounds

The worker is spawned by delegate_task, a tool that hands one job to another profile. Two bounds matter:

delegation:
  max_concurrent_children: 3
  max_iterations: 50

max_concurrent_children caps how many parallel subagents a single caller can spawn. max_iterations caps how long the worker iterates before returning. Both are deliberate limits: the worker is a workhorse, not an open-ended loop.

Step 5 — Dispatch work

From any profile that has delegation in its toolset, a single unit of work is one call:

delegate_task(goal="Uninstall SearXNG and clean up all traces", context="...")

For parallel work, batch them in one call:

delegate_task(tasks=[{goal: "...", ...}, {goal: "...", ...}])

The batch runs children in parallel, capped by max_concurrent_children. The caller emits one delegate_task regardless of how many items there are; the fan-out complexity is absorbed by the worker.

The verifiable-handle rule

This is the load-bearing rule, and it is the reason a worker you cannot watch is safe to run at all. The worker returns one consolidated message: a summary, a list of verifiable handles, and how it verified each one. A handle is anything the caller can stat or grep to confirm the work happened: a file path, an exit code, a byte count, a stopped service, an empty grep result. “I did X” is not a handle. A deleted directory is a handle.

The worker is asked to verify its own work before returning: a file it wrote exists, a command it ran exited zero, a URL it fetched has the content it said. But the caller checks too. The contract is explicit: expect spot-checks, do not argue with them, recover by producing the missing handle. The return value is a promise; stat, grep, and ls are the proof.

What broke, and what it taught me

Surprise 1 — worker-spawned workers do not run in parallel

The fan-out rule looks like it should give you parallelism everywhere. It does not. Inside a worker, a delegated sub-subagent runs synchronously: the worker’s turn blocks until the child returns. The background parameter is deliberately ignored for any delegation from a subagent, so children from depth one and below run one at a time.

The practical rule: if you need real parallelism, batch at the top level with delegate_task(tasks=[...]), and do not expect the worker to fan out for you. The win is that the caller emits one call, not that the wall clock shrinks. Do not promise any speedup from a worker fanning out.

Surprise 2 — the caller must spot-check the worker

On 2026-08-02, a delegation from my system-admin profile returned an interrupted exit after 6.5 seconds: one API call, four terminal steps, no result summary. It was killed, not crashed. That is the failure mode that makes the rule real. A worker you cannot watch needs its output verified, and the verification is the caller’s job, not the worker’s promise.

The cost that only shows up in operation

Now the part I left out of the first version of this article, because I had not paid it yet.

The design above is sound and the build is reproducible. What is not in the config is what it costs to run, and the only way to find that out is to run it. I ran it as the fleet’s default shape: interactive profiles stripped to orchestration, every operational task routed to the worker through the board. Within days, three costs appeared, and none of them were visible in any config file.

Latency on trivial work. A stripped profile has no terminal, no file, no web. Every lookup, every grep, every “check this path” becomes a task round-trip: create, dispatch, wake, verify, return. A five-second check became a ticket to a colleague who was also busy. Do that forty times a day and the fleet stops feeling like a fleet and starts feeling like a queue.

Context hops, not context hygiene. The split was supposed to keep contexts clean. For anything small it did the opposite: the caller’s context now had to contain the delegation decision, the task spec, and the verification of a result it could have produced inline in one tool call. Big jobs still earned the clean context. The daily grind mostly paid delegation ceremony.

A wake gap that swallowed results. The worst one. When a delegated task completed, the creating profile was supposed to relay the completion back. On 2026-08-09, the day this article first went up, my personal profile’s gateway sessions went silent after dispatching subagents. The subagents finished. The results re-entered the transcript. Nothing woke the parent session to relay them, and I had to ask what happened. Work completed and nobody was told.

That is why the verdict in my removal plan said it was a nightmare in reality. The worker itself still exists on my box, dormant. What got removed was the mandate.

The decision rule that survived: delegate when the job genuinely benefits from a fresh isolated context, long runs, noisy multi-step execution, parallel workstreams. Execute inline for everything else, with the full toolset in the profile that already has the context.

The recipe

Everything above, in one place.

  1. Create the empty profile.

    hermes profile create agentic-worker --no-skills --no-alias --description "Headless bulk-work subagent."
  2. Kill the chat surface.

    hermes config set platforms '{}'
  3. Add to config.yaml:

    memory:
      provider: mnemosyne
      memory_enabled: false
    platform_toolsets:
      cli:
        - file
        - terminal
        - delegation
        - code_execution
    delegation:
      max_concurrent_children: 3
      max_iterations: 50
  4. Dispatch from a caller profile that has delegation in its toolset:

    delegate_task(goal="<one focused job>", context="<enough to reproduce the setup>")
  5. Batch parallel work at the top level, never inside the worker:

    delegate_task(tasks=[{goal: "...", ...}, ...])
  6. Verify the handles the worker returns. The return value is a promise; stat, grep, and ls are the proof.

The takeaway

The headless worker is not a chat agent with its mouth taped shut, and it is not a fleet shape. It is a capability: no persona, no memory, no channel, four tools, and a contract that its output must be verifiable. Use it for the jobs that genuinely earn a fresh context, and keep your interactive profiles able to do the trivial work themselves.

The verifiable-handle rule survives everything. It is why a worker you cannot watch is safe to run, and it is the same discipline on the caller’s side: the return value is a promise, and the handles are the proof.

I built this, documented it, ran it as a mandate, and removed the mandate. The build is here so you can reproduce it. The removal is the better half of the story: how I reversed the pattern safely, one profile at a time, and which parts of the worker I kept. That is the sequel, hermes-17, “The day I removed the orchestrator pattern I’d just documented”.


Michael Short is the founder of The Agent Files.

← All transmissions