"Deep agent" sounds like a new kind of model. It isn't. It's a way of organising work around an ordinary one so it can handle long tasks without losing the plot:
- a planner turns the task into a short to-do list,
- subagents each take one item, with a fresh context,
- notes and results live outside the prompt,
- a final step puts it together and says what's missing.
![Anatomy of a deep agent: planner, parallel subagents, shared state, writer]
In this post we'll build one for a job that's tedious for people and a good fit for the pattern: answering a security questionnaire from your own documentation. A customer sends twenty questions ("Do you encrypt data at rest?", "Where is data hosted?", "Do you support SSO?"). The answers are somewhere in your docs. Someone has to find each one.
I work on DNotifier, so the code uses its workflow API. The structure carries over to anything that lets you run agents in parallel.
Why not just one agent?
You could give one agent a search tool and the whole questionnaire. It would work for three questions. By question twelve its context holds eleven sets of search results, most of them irrelevant to question twelve. It starts mixing answers up, or skipping questions because it "feels" done.
The subagent split fixes that directly. Each subagent sees one question and the search results for that question. Nothing else.
![Context isolation: each subagent sees only its own task and results; the writer sees only summaries]
What you need
- Node 18+,
npm install @dnotifier-realtime/dnotifier - A DNotifier app on the Starter plan with a model provider key added
- Your security docs, policies or past questionnaire answers uploaded to the app's knowledge base
Agent 1: the planner
The planner reads the pasted questionnaire and returns the questions as a JSON list. Asking for JSON means code, not prose, decides what happens next.
// questionnaire.mjs
import { DNotifier } from "@dnotifier-realtime/dnotifier";
const textOf = (res) => res?.data?.content ?? res?.content ?? "";
const MAX_QUESTIONS = 8;
const planner = DNotifier.defineAgent({
name: "planner",
async run(ctx) {
const res = await ctx.sendAI({
message: {
useKnowledgeBase: false,
messages: [
{
role: "system",
content:
`Extract the distinct questions from this security questionnaire, at most ${MAX_QUESTIONS}. ` +
"Merge duplicates. Keep each question short and self-contained. " +
'Reply with JSON only: {"todos":["question 1","question 2"]}',
},
{ role: "user", content: String(ctx.input) },
],
},
saveHistory: false,
label: "Plan questions",
});
try {
const { todos = [] } = JSON.parse(textOf(res));
return { todos: todos.slice(0, MAX_QUESTIONS) };
} catch {
return { todos: [] }; // the workflow handles an empty plan
}
},
});
The cap matters. A planner left uncapped will happily split one question into five. Every to-do item becomes at least two model calls later, so the cap is also your cost ceiling.
Agent 2: the answerer (a subagent)
One question in, one short answer out, from the knowledge base only.
const answerer = DNotifier.defineAgent({
name: "answerer",
async run(ctx) {
const question = String(ctx.input);
const hits = await ctx.search({ query: question, limit: 4, minSimilarity: 0.5, label: "Search docs" });
const res = await ctx.sendAI({
message: {
useKnowledgeBase: false,
messages: [
{
role: "system",
content:
"Answer the security question using only these excerpts from our documentation. " +
"Two sentences maximum, factual, no marketing. " +
'If the excerpts do not clearly answer it, reply exactly "NEEDS REVIEW".\n\n' +
JSON.stringify(hits),
},
{ role: "user", content: question },
],
},
saveHistory: false,
label: "Answer one question",
});
const answer = textOf(res).trim();
return { question, answer, needsReview: answer.includes("NEEDS REVIEW") };
},
});
Two choices here are deliberate:
- "Only these excerpts". For a security questionnaire a confident wrong answer is worse than no answer. If the docs don't say it, a person should.
- A fixed "NEEDS REVIEW" string. Code can check for it. A polite "I couldn't find that" can't be counted reliably.
Agent 3: the writer (the second subagent)
The writer never sees search results. It gets the list of question/answer pairs and formats them.
const writer = DNotifier.defineAgent({
name: "writer",
async run(ctx) {
const { answered } = ctx.input;
const res = await ctx.sendAI({
message: {
useKnowledgeBase: false,
messages: [
{
role: "system",
content:
"Format these questionnaire answers as a Markdown table with columns Question | Answer. " +
"Do not change the meaning of any answer. Do not add new claims.",
},
{ role: "user", content: JSON.stringify(answered) },
],
},
saveHistory: false,
label: "Format answers",
});
return { table: textOf(res) };
},
});
The workflow: plan, fan out, check, write
export const questionnaire = new DNotifier.Workflow({
name: "security-questionnaire",
description: "Plan questions → answer each in parallel → flag gaps → format",
observability: true,
async entry(ctx) {
const { todos } = await ctx.runAgent("planner", { input: ctx.input });
if (!todos.length) return { error: "No questions found in the input." };
ctx.state.todos = todos; // the plan lives here, outside every prompt
// One subagent per question, all at once.
const { results } = await ctx.runAgents(todos.map((q) => ({ name: "answerer", input: q })));
ctx.state.results = results;
// Plain code splits answered from needs-review. No model involved.
const answered = results.filter((r) => r && !r.needsReview);
const review = results.filter((r) => !r || r.needsReview).map((r) => r?.question ?? "(failed)");
const { table } = await ctx.runAgent("writer", { input: { answered } });
return { table, needsReview: review, answeredCount: answered.length, total: todos.length };
},
}).registerAgents({ planner, answerer, writer });
And run it:
const notifier = new DNotifier({
appId: process.env.DNOTIFIER_APP_ID,
secret: process.env.DNOTIFIER_SECRET,
transport: "http",
userId: "questionnaire-worker",
});
await notifier.connect();
const pasted = `
1. Is customer data encrypted at rest and in transit?
2. In which regions is data stored?
3. Do you support SAML SSO?
4. How long are logs retained?
`;
const run = await notifier.runWorkflow({ workflow: questionnaire, input: pasted });
console.log(run.result.table);
console.log("Needs a person:", run.result.needsReview);
console.log("executionId:", run.executionId);
What's actually happening
- The plan is stored in
ctx.state, so it's visible in the run trace and doesn't depend on any model remembering it. -
ctx.runAgentsstarts one answerer per question in parallel. Four questions take roughly as long as one. - Each answerer's context is just its question plus four excerpts. Question 4 never sees question 1's search results.
- Code, not a model, decides which answers go to a person. That's the bit I'd least want a model to do.
- The writer sees short answers only, so its prompt stays small whether you send four questions or forty.
Open the run in the dashboard by its executionId and you'll see every step separately: the plan, each search, each answer, the format step. When an answer is wrong, you can tell straight away whether the search missed or the answerer misread a good excerpt. Those need different fixes.
Guardrails before you use it for real
- Cap the plan (we used 8). Bigger questionnaires: split them in code into batches and run the workflow per batch.
- Never re-plan in a loop. If you add a retry for NEEDS REVIEW items, do it once, with a rephrased question, and stop.
- Keep subagents read-only. Nothing here sends or changes anything, so a failed run can just be started again.
- Run it from a worker. Long questionnaires take a while. Start it from a background job, not an open HTTP request, and notify the person when it's done.
- A person signs off. The output is a draft for whoever owns security answers. It saves the search, not the responsibility.
Where "deep agents" comes from
The name was made popular by LangChain's open-source deepagents package, which packages planning, a virtual file system, subagents with isolated context and context compression into one ready-made package. If you work in that ecosystem, it's well worth a look. The workflow above shows the same pattern with no extra package: you write the planner prompt and the limits yourself, and every step stays visible and testable.
More on the pattern, including how deep agents differ from fixed multi-agent workflows: What Are Deep Agents?


Top comments (0)