Turning the agent from a single answerer into an orchestrator: multi-step workflows with chained tools, task decomposition and planning, plus error handling, retry patterns, and fallback actions so the workflow still completes even when one step fails.

In episode 9 you learned how to keep multi-turn conversation context alive and under control. But an agent that only answers questions has not done any work. Episode 10 raises the bar: you will make the agent complete multi-step jobs — planning, calling tools in a chain, and recovering on its own when a step fails.
Here is the roadmap for this episode: building multi-step workflows with chained tools, task decomposition and planning, and then error handling, retry patterns, and fallback actions.
In episode 6 you saw a simple chaining pattern: web.search then web.fetch. Orchestration takes that pattern to the next level — flows with many steps, decisions along the way, and per-step failure handling.
A classic example: an agent that handles a complaint ticket.
1. cari user di database -> db.query
2. ambil riwayat order terakhir -> db.query
3. cek status pengiriman -> http.get ke tracking API
4. simpulkan penyebab komplain -> model
5. susun jawaban + saran -> model
6. kirim balasan ke user -> http.post (perlu konfirmasi)Notice there is a decision: if the shipping status is not found in step 3, the flow must not continue to step 4 using empty data. This is what distinguishes orchestration from merely calling many tools at random — every step knows which step preceded it and what to do if the result is not as expected.
Hermes lets you define workflows declaratively inside the agent config. Every step has an id, the tool it uses, and the rules for moving forward.
workflow:
steps:
- id: find_user
tool: db.query
args: { query: "SELECT * FROM users WHERE id = {userId}" }
- id: last_order
tool: db.query
args: { query: "SELECT * FROM orders WHERE user_id = {lastUserId} ORDER BY created_at DESC LIMIT 1" }
- id: check_shipping
tool: http.get
args: { url: "https://api.courier.example.com/track/{order.tracking}" }
- id: summarize
tool: model.generate
args: { prompt: "Rangkum status komplain dari data di atas" }Every step uses the previous step's output through placeholders, so the flow is explicitly chained. This version uses the {userId} pattern for values coming from user input, and {order.tracking} for the result of a previous step. A declarative workflow has a big advantage: it is easy to read, easy to version-control, and easy to test step by step. Parameters like max_attempts for retry limits will be added later in the error handling section.
Not every job can be written as a static workflow. When a user gives a big, open-ended command, the agent must break it down itself — this is called task decomposition. Hermes does this through planning mode: before executing anything, the agent puts together a plan of subtasks and their order.
hermes run agents/ops.yml --plan --session ops-1With the --plan flag, the controller has the agent write a plan first: a list of subtasks, the tools each subtask needs, and the dependencies between subtasks. Only after the plan is approved does execution begin. This prevents the agent from "shooting from the hip" and skipping important steps.
A healthy decomposition pattern:
Info
Combine planning with runtime.max_iterations from episode 3: the plan limits what is done, max_iterations limits how many tool-call loops may be used — two guards that complement each other.
A workflow step can fail for many reasons: a tool error, missing data, an API timeout, or the model rejecting a format. Error handling in orchestration means defining what happens per failure type, rather than letting the whole workflow stop.
on_error:
not_found:
- match: { tool: "db.query", error: "no rows" }
action: complete
response: "User tidak ditemukan. Minta email untuk verifikasi."
timeout:
- match: { error: "timeout" }
action: retry
max_attempts: 2
backoff_ms: 1500
rate_limited:
- match: { error: "rate_limit" }
action: fallback
fallback: cache_answersSeparate the handling by error type: missing data is a normal condition (complete with a safe answer), timeouts deserve a retry, rate limits are routed to cached answers. Unknown errors fall into a default handler: stop and report to the user, do not keep processing with empty data. Values like backoff_ms determine the initial delay before a retry.
Retries must not be done haphazardly. Three patterns you need to know:
export async function withRetry(fn, config) {
let delay = config.baseMs;
for (let attempt = 0; attempt <= config.maxAttempts; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt === config.maxAttempts) throw err;
const jitter = Math.random() * delay * 0.3;
await sleep(delay + jitter);
delay *= 2;
}
}
}A fallback action is the last safety net: if the retry also fails, the workflow must not stop without a result. Examples: cache_answers for rate limits, a "the service is busy, please try again later" answer for repeated timeouts, or escalation to a human for unrecoverable errors. Choosing the right fallback keeps the agent useful even when the infrastructure behind it is failing — a topic we will dig into with circuit breakers in episode 14.
Warning
Do not retry errors that are permanent, such as failed validation or missing data. Retry only for transient failures: timeout, rate limit, dropped connection.
Orchestration turns the agent into a real worker: a declarative workflow chains tools together with each step's output flowing into the next; task decomposition breaks big commands into a plan; and error handling, retry, and fallback make the flow still complete even when a step fails.
Key takeaways:
--plan forces the agent to assemble subtasks before executing.In episode 11 we secure all of this power: Security & Access Control — securing tool access and API keys, restricting capabilities to safe operations, plus audit trails and policy enforcement. See you there!