
Articles · Research · 18 min
How do I have an AI agent write a weekly research brief unattended?
Have an AI agent write a weekly research brief unattended by putting the job on a cron, locking the sources, writing a memo a script can test, and pausing at a human review queue. The person sits at the edge of the loop. They do not click every tool. LangGraph durable runs hold the checkpoint. OpenAI Agents SDK Sessions hold last week. Anthropic’s human checkpoint is the queue.
By Eric · Rome · Aug 29, 2026
Monday starts with a one-page research brief in a folder, sources attached, last week named, waiting for a person to read it. A chat window that asks what to do next is still a chatbot. A weekly brief is a job with a clock, a source list, a memo shape, and a queue, and the person is at the edge.
A weekly research brief is a file with a finish line: a cron starts the run, the agent reads sources you named, and it writes a memo in a shape you can test. A person reviews that memo in a queue, not inside every tool call. Unattended means the owner designed the loop.
What the weekly brief has to be
The brief is an artifact that lives at a path, names the week and the audience, and cites source ids the run actually fetched. If a script cannot tell that this happened, a person will stay in the inner loop forever. A thread is not a brief.
I treat the weekly brief as the first unattended research job on a desk because it is reversible: a draft can be rewritten, and a send to customers cannot. Recurring work also gives you a frozen set of last eight Mondays. That set is how you know the loop holds before you take yourself out of every click.
You do not have a brief if you cannot say what the file contains. You have a Sunday conversation.
I already wrote what is a research agent as the job definition. The weekly brief is that agent pointed at a clock.
| Chat summary | Weekly research brief | |
|---|---|---|
| You give it | A prompt | A job with a week id and a finish line |
| It reads | Whatever it finds | Sources you named |
| It writes | A reply | A memo at a path, with source ids |
| You are done when | The reply looks useful | The file exists and the queue has an item |
| The person | Types the next turn | Reviews the file, then releases it |
Anthropic’s Building effective agents (December 2024) draws a line I keep on the wall: a workflow is a predetermined path, and an agent is a model that directs its own tool use. The weekly brief is both. Cron, schema, and queue are the path, and gathering from messy sources is the part you cannot fully draw in advance.
Cron, then sources, then memo, then a human queue
The route is four stages: a schedule starts the run, the agent fetches allowlisted sources and stores the raw records, it writes a memo from those records, and the runtime parks that memo in a human review queue. Nothing after the queue is automatic until the score says it can be.
Cron is the clock, not the brain. I use one job per ISO week, with a thread id tied to that week.
The job exits when the memo already exists for that id. When the last run died mid-fetch, the job resumes from the checkpoint. A retry storm that writes five briefs for the same Monday is a miss, even if each brief is pretty.
Sources come next, and they come first in the trace. The agent does not outline then hunt: it fetches, then writes, and raw fetches live under /runs/{week}/sources.json. The memo may only cite ids in that file, because a citation to a page the run never retrieved is a fabrication with a footnote.
The memo is a packet: title, week, audience, delta versus last week, claims with source ids, open questions, recommended next action, and what was not found. I cap the body so the desk can read it on Monday.
The runtime then interrupts. A person approves, edits, or rejects. The agent does not mail the list on the same grant that wrote the draft.
- Cron: one job, one week id, a spend cap, a step cap, an idempotent exit.
- Sources: named tools, raw records on disk, no open web by default.
- Memo: a schema a script can check, source ids only, a word cap.
- Queue: an interrupt with the file path, the spend, and the open questions.
Write a finish line a script can see
Done is not a vibe: it is a path, a schema, and a handful of checks. If only a person can tell the brief finished, that person will sit inside every Sunday. Machine-checkable done is how you take humans out of every loop without abandoning the job.
I write the test before the prompt. The file exists at /briefs/{week}.md, front matter has week, audience, and generated_at, and every claim line has a source_id in sources.json.
The memo names last week’s file or writes “no prior brief,” open questions are a list, and word count sits under the cap. Fail any of those and the run is a miss, even if the prose sings.
That is the same rule I use in how to put an AI agent in production. The weekly brief is a small production job: a finish line, tools, a stop, a score, and permissions. The brief inherits all five or it stays a demo you babysit.
Name the audience in the done condition. A founder brief is not a sales brief, and a sales brief that buries pricing changes is a fail even if the citations are clean. The script cannot judge taste, but it can judge whether the required beats are present or marked not found.
Marking “not found” is success, and inventing the beat is a fail.
Allow only the sources the job needs
The tool list is the prompt you cannot hide: name the feeds, the filings, the CRM view, and last week’s brief, each with a schema, a timeout, and a permission. Search is not browse, and read is not send. Extra tools are how the Sunday job becomes a wander through the open web.
I start with four tools and I fight to keep it there: fetch_feed, fetch_filing, read_store, read_last_brief. If the desk later needs a fifth, I add it after the score says the four are not enough.
Open-ended browsing is a last resort and a different grant. Anthropic’s December 2024 note is blunt about tool design: the description is the interface. Enums beat free text, and errors the model can use beat a stack trace.
How you build a research agent is mostly this list, and the model is downstream.
A research agent with thirty tools and no allowlist will find something every week, and you will not be able to score it. Tight tools are how a weekly job stays a weekly job.
- Allowlist domains and feed ids in code, not in the prompt.
- Persist the raw fetch before the model writes a sentence.
- Return short true results: not found, denied, timeout, or the record.
- Keep write tools off this worker. The queue writes the released file.
Put the run on a durable runtime
A Sunday job that dies mid-fetch is not unattended. It is a pager. Durable execution saves progress at key points so the run can pause and later resume where it left off, which LangGraph’s docs describe as the point of a checkpointer: recover from a failure, wait on a human, and avoid redoing work that already finished.
LangGraph’s durable execution guide (2026) is the contract I want for this job. Compile the graph with a checkpointer, pass a thread id for the week, and wrap side effects (the fetches, the file write) inside tasks so a resume does not hit the same API twice.
YouTube
Open originalLangChain’s LangGraph: Persistence, 5 Feb 2024, is the checkpointer and thread-id pattern. A weekly brief that dies mid-fetch needs that persistence, or Monday starts from empty.
Durable execution is on when you are already using a checkpointer. Without one, the brief is a script you hope finishes.
LangChain’s LangGraph durable execution documentation (2026) states the mechanism: the persistence layer saves each step to a durable store, and a workflow interrupted by a crash or a human can resume from the last recorded state. It also notes you can resume without reprocessing previous steps after a long delay, including a week later. That sentence is the weekly brief.
Three requirements sit under that sentence: a checkpointer that actually persists (Postgres in production, not an in-memory saver that dies with the process), a thread id you can reuse (brief-2026-W34, kept under 255 characters), and side effects wrapped so replay is deterministic. Fetching a feed inside a raw node, with no task boundary, is how you double-charge an API and then argue with the memo.
LangGraph names three durability modes: exit, async, and sync. Exit writes only when the graph stops, which is weak if a worker dies mid-fetch.
For a brief that will sit in a human queue I use a persistent checkpointer and I do not pick exit. The runtime also owns the stop: step cap, spend cap, timeout.
Anthropic’s December 2024 essay says it is common to include stopping conditions. The model does not get a vote on those caps.
Sessions hold the week, checkpoints hold the run
Two memories, two jobs: a checkpoint is the state of this week’s graph so a crash or a human pause can resume, and a session is the history across weeks so this Monday can name last Monday. Mix them and you either replay a half-written memo or forget the delta you asked for.
OpenAI Agents SDK Sessions documentation is the second contract: provide a Session to the runner. Before each run, the SDK fetches stored items and prepends them, and after each run it persists new user input and assistant output. You stop stitching history by hand, and you get a way to resume an interrupted run with the same session id and the same backend.
OpenAI Agents SDK Sessions (Python docs, current in 2026) describe that loop, and they warn you not to combine a session with run-level continuation options such as conversation_id in the same run. Pick one strategy.
For the weekly brief I keep a session per desk, named like research-brief-operator, backed by SQLAlchemy or SQLite in development. The session holds released memos, not raw HTML.
Last week lives in the session so this week can write a delta. The SDK’s SessionSettings let you fetch only the most recent N items, and four released briefs is enough. If a run pauses for approval, resume it with the same session id and the same store.
That is in the OpenAI guide, not a folk rule. Queue, week, and crash recovery all need that session. If you only have a chat log, you have none of them.
| Checkpoint (this run) | Session (this desk) | Store (the facts) | |
|---|---|---|---|
| Holds | Graph state for one week id | Released memos and decisions | Source records, allowlists, scores |
| Survives | Crash, interrupt, resume | Monday to Monday | Across threads |
| I use it to | Finish W34 after a timeout | Write the delta versus W33 | Cite only what was fetched |
| Fails when | In-memory saver, no thread id | You stuff raw HTML into history | The memo invents an id |
LangGraph’s persistence docs (2026) split the same idea: checkpointers for thread-scoped memory, stores for cross-thread facts. I put source records and allowlists in a store.
I put the running graph in a checkpointer. I put the readable history of briefs in a session if I am on the OpenAI runner. The brand of the model matters less than whether Monday can find Sunday’s work.
Pause at a human checkpoint, not inside every tool
Unattended is not unsupervised: the person sets the goal, the grants, and the review, and they do not approve every fetch. Anthropic’s Building effective agents (19 December 2024) says agents can pause for human feedback at checkpoints or when they hit blockers. That checkpoint is a queue, not a chat you babysit.
Agents can then pause for human feedback at checkpoints or when encountering blockers.
Anthropic published that essay on 19 December 2024. The surrounding paragraph also tells you to take ground truth from the environment at each step, and to include stopping conditions.
For the weekly brief, ground truth is the fetch result. The checkpoint is the queue after the memo exists. The blocker is a source that 500s, a spend cap, or a schema fail.
LangGraph’s interrupt docs (2026) give the mechanism. Call interrupt() inside a node with a JSON payload, the graph saves state through the checkpointer and waits, and resume with Command so that value becomes the return of interrupt().
You need a durable checkpointer and a thread id or there is nothing to resume. Static breakpoints at compile time are for debugging. Dynamic interrupts are for the queue.
LangGraph interrupts documentation (2026) is the page I keep next to the Anthropic essay. The payload I send to the queue is week id, memo path, source count, spend, open questions, and the done-test result. The reviewer sees a file, not a trace, and if they need the trace, the score already failed.
LangChain’s human-in-the-loop middleware (2026) names the decisions I want: approve, edit, reject, respond. Approve releases the file, edit patches then rechecks the schema, and reject returns one round of comments.
That is the same line as how to run AI agents without a human. People stay at the edge. The worker can write /drafts/{week}.md, and it cannot write the released path or post to the list.
The memo is a packet, not a chat dump
Fleets die at handoff, and solo weekly jobs die the same way when the memo is a recap of the trace. Pass a packet: the original finish line, the evidence (file ids, not vibes), the next action, and the limits left. The incoming reader continues from that packet and does not re-ask the whole week.
I reuse the packet I already use for how to hand off an agent to a human: goal stays the original job, evidence is source ids and the path to sources.json, next action is one line, and limits are spend left, retries left, and the grants this worker still has. Shared memory is a pile. The packet is a decision.
The body under the packet is short: what changed versus last week, what stayed true, claims each with a source id, open questions, and what was not found on purpose. Recommended next action is one line for a human, not a paragraph of strategy. If a sentence cannot point at a source id or at “not found,” it does not belong in the brief.
Do not paste the trace into the memo. The trace is for Arena and for failure review, and the memo is for the desk that has twelve minutes on Monday. A brief that reads like a chain of thought is a model talking to itself in public.
How to ship the weekly brief
Six moves, in order: write done, lock sources, put the job on a clock and a thread, fetch then write, interrupt into a queue, and score frozen weeks before you skip that queue. Skip a step and you have a scheduled chatbot with a folder.
- 01
Name the artifact and the done test
Pick the path and the schema. Checks: file exists, week id matches, every claim has a source id, last week is named or explicitly missing, under the word cap. If a script cannot fail the run, a person will have to.
- 02
Allowlist the sources and the tools
List the feed ids, filing types, warehouse view, and read_last_brief. Each tool gets a schema, a timeout, and a permission. No open internet on this worker. No send. Add a tool only after the current list fails the score for a reason you can name.
- 03
Schedule the cron and a thread id
One job per ISO week. thread_id is brief-{year}-W{week}. Exit when that thread already has a released memo, and resume when it has a checkpoint. Cap spend and steps in the runtime. Wrap fetches so a resume does not repeat a side effect.
- 04
Fetch sources, then write the memo
Run the fetch tools first. Persist sources.json. Write the memo from those records. The model may not introduce a url absent from the file. Not found is a valid line. A confident paragraph with no id is a fail.
- 05
Interrupt into a human review queue
When the done test passes, interrupt with the memo path, the spend, and the open questions. A person approves, edits, or rejects. Approve copies draft to the released path. The worker still cannot distribute.
- 06
Score frozen weeks before you skip the queue
Keep eight to twelve past weeks on ice. Grade the artifact, not the chat. Promote skipping the queue only where groundedness, coverage, and source quality hold. Autonomy is a grant, earned per job.
I ship this on one desk for a month before I clone it. The first weeks a person reads every memo, and when the misses are boring I stop sitting inside the trace and still open the queue. The queue is cheap, and a bad brief in a customer channel is not.
Score last week’s briefs before you trust this week’s
Pretty traces that miss the file are zeros. Freeze weeks you already know, run them more than once, and score the artifact on groundedness, coverage, and source quality. Do not grade the essay in place of the file, because research quality is judged relative to the task.
Anthropic published Demystifying evals for AI agents on 9 January 2026. In the research-agent section they recommend combining grader types: groundedness checks (claims supported by retrieved sources), coverage checks (key facts a good answer must include, or an explicit miss), and source quality checks (authoritative sources, not the first hit).
Exact match is for facts that have a right answer. A model judge is for synthesis, and that judge needs periodic human calibration.
I map those three graders onto the weekly brief. Groundedness: every claim line has a source_id in sources.json, and a string check confirms the claim is not empty.
Coverage: required beats for this desk (pricing, competitor, regulation, or whatever the audience named) are present or marked not found. Source quality: every url host sits on the allowlist.
Fail source quality and the run is a miss even if the sentences are true. A lucky citation is not a loop.
The outcome is the file and the queue item, not the transcript. Anthropic’s eval essay makes that split explicit: a flight agent that says “booked” is not done if the reservation does not exist.
A research agent that narrates a thorough search is not done if the file is missing or the claims do not resolve. I log every tool call. I refuse to grade the essay in place of the file.
This is why the related system on this site is Arena: same week, competing traces, the artifact as the score.
I swap a model only when the new loop beats last week’s agent on the frozen set. The first real Monday is not a test set.
Early on, those weeks are the eval set. Once they pass, they become a regression suite you run before you change the allowlist.
| Grader | Pass | Fail |
|---|---|---|
| Groundedness | Each claim has a source id the run fetched | A sentence with no id, or an id not in sources.json |
| Coverage | Required beats present or marked not found | A required beat skipped, or invented |
| Source quality | Hosts on the allowlist | A citation to a domain you never granted |
| Done test | File at the path, schema valid, under cap | Pretty markdown in the trace, nothing on disk |
I run the frozen set more than once when I change the prompt. Misses should be schema and fetch, not invention. I do not average my way past a fabricated citation.
What usually breaks
The failures are boring, which is why people skip them: a scheduled chatbot with no finish line, an open web tool, an in-memory saver, a session stuffed with HTML. A person who still clicks every fetch, or a queue that auto-sends, is the same miss. Read the list as a checklist, not as drama.
- No finish line, so the job chats until the budget dies.
- Open browsing on the same grant as the memo, so the brief cites a blog the desk never trusted.
- In-memory checkpoints, so a worker restart on Sunday night loses the fetch.
- No thread id, so Monday starts a second brief for the same week.
- Session history unbounded, so March rumors show up as this week’s delta.
- Human in every tool call, so you built a slow assistant and called it unattended.
- Human nowhere, so a bad citation ships because nobody owned the queue.
- Eval after launch, so the first real Monday is the test set.
- One worker that can read sources and mail the list.
A source that is down is not a drama if you designed for it. The fetch tool returns timeout or not found. The memo writes “source X down, beat Y not found.”
The done test still passes if the schema allows an explicit miss. The queue still gets an item. What you do not do is let the model fill the hole with a remembered number from training.
When Monday is boring, promote the job
Unattended is earned per job, not flipped on for the company. When Monday is boring, the file is there, the score holds, and the edits are typos, you promote. You do not promote because the demo was smooth, and autonomy is a grant you can revoke.
Promotion is a grant change, and the worker still cannot send. The queue can become a sampling queue: every brief still writes, one in four still pages a person, misses always page. If the score drops, the grant shrinks.
The person at the edge still sets the audience, the allowlist, and the required beats. They read the weekly score. They do not sit inside the trace unless it failed.
If a script with no model would do (one RSS feed, a template, a mail), write the script. Anthropic’s December 2024 guidance is to find the simplest solution and add complexity only when it improves the outcome.
An agent is for the part you cannot fully draw: which of twelve filings matter this week, how the delta reads, which open question is real. The cron, the schema, and the queue should stay dull.
Questions
Only after the job holds on a frozen set of past weeks, and only as a grant you can revoke. Draft is still not send. A sampling queue is safer than a silent send.
The fetch tool returns timeout or not found. The memo marks that beat as not found. The done test can still pass. The model does not fill the hole from memory.
No. A scheduled chat still produces a reply. A brief is a file with a schema, allowlisted sources, a checkpoint you can resume, and a queue. If you still copy-paste the answer into a doc, you did not finish the job.
As few as the desk needs. I start with four named tools and an allowlist. Add a source when the frozen weeks miss a required beat for a reason you can name, not because a demo wanted the open web.
Next

