A scheduled agent can quietly become less useful long before it crashes. A GitHub token expires and the brief says there are no updates. A Slack post times out after it was delivered, so the next run sends the same brief again. Or someone changes a preference, but an old copy embedded in the prompt keeps winning.
Lance Martin and CJ Avilla’s Building Effective Agent Automations walks through a weekday brief built with Claude Managed Agents (beta). It reads selected Slack channels and GitHub pull requests, decides what matters today, and posts to one Slack channel. The point is not that this exact stack fits every team. It is that unattended automation needs a clear contract for state, failure, permissions, and delivery.
The architecture overview below is from the original article. I have placed an optimized local copy in this project; the source is credited in References.

Think in components, not in one large prompt
The example has six responsibilities: sources to read, one destination to write to, a versioned agent, a schedule, memory, and guardrails. In practice, these are separate configuration resources rather than a single prompt file. That separation makes it possible to change a schedule without rewriting source rules, or rotate credentials without putting secrets in the agent instructions.
| File or resource | What it controls |
|---|---|
agent.md | Model, tools, and the instructions for each run |
deployment.md | Schedule, time zone, first message, budget, vault, and memory stores |
environment.yaml | Which network hosts the sandbox may reach |
memory_store_preferences.yaml | Reader-owned settings, mounted read-only |
memory_store_state.yaml | Bookmarks, ledger, notes, proposals, and run records |
vault.yaml | The credential vault attached to the deployment |
slack/manifest.yaml | The Slack app and its scopes |
claude-lock.json | Resource IDs recorded by ant apply |
ant apply reads the configuration, creates the resources in the API workspace, and records their IDs in claude-lock.json. The scheduled deployment runs on Anthropic’s infrastructure, so a laptop or a self-managed server does not need to stay online.
The article’s guided setup starts by updating Claude Code and asking the claude-api skill to walk through the post. The skill reads the article, proposes a setup, writes configuration under an agents/ directory, and applies the resources; treat that output as a starting point and adapt it to your own sources and preferences:
claude update
/claude-api managed-agents-onboard https://claude.dev/blog/building-effective-agent-automations/
The reference repository also provides a setup script. Its quickstart creates a Slack app, a fine-grained read-only GitHub token, and the Managed Agents resources; it adds the credentials to the vault separately, attaches the vault to the deployment, and keeps the schedule paused until a run has been checked. The preference store is created by deployment setup, but the preferences.md file is not. Seed that file before expecting the first run to follow your channel, repository, destination, and length choices.
The repository’s Slack manifest uses channels:history and chat:write; the bot must be invited only to the channels it needs. Its GitHub token is scoped to repository contents and pull requests with read access. The setup creates the vault with ant apply vault.yaml, adds each credential through the TypeScript SDK, then attaches the vault ID from claude-lock.json to vault_ids in the deployment. For Slack, the credential is limited to slack.com and injected into the request header. At the time of the reference quickstart, the ant CLI prerequisite was version 1.34 or later, authenticated with ant auth login or an API key, plus jq—check the current setup guide because these requirements can change.
Sources should be readable, scoped, and recoverable
The sample reads Slack channels and GitHub pull requests selected in the reader’s preferences. It treats both as inputs: GitHub gets a read-only token, while Slack access is limited to channels where the bot is a member. The template can be extended to other sources, but each new source needs its own access boundary and progress marker.
The credentials do not live in the agent’s sandbox. For GitHub, the agent calls an MCP server through a proxy that matches the server URL to a credential in the vault. For Slack, the agent calls the API with curl from its shell. It sees an opaque $SLACK_BOT_TOKEN placeholder; the platform injects the real value only when the request goes to an allowed host. The secret itself stays in the vault, outside the sandbox.
The source article’s diagrams highlight how the read-only sources and vault fit together. This copy is stored locally rather than hotlinked.

This distinction matters when a source fails. A fixed “last 24 hours” query can leave a gap when a run is late and repeat items when it runs early. The implementation instead keeps a bookmark for each source (or each Slack channel). On the next run it reads from ten minutes before that bookmark, then skips IDs already in the ledger when their status has not changed. The overlap protects against coarse timestamps and delayed visibility; the stable ID and ledger prevent the overlap from becoming duplicate reporting.
The first run has no bookmark, so it starts with the previous 24 hours. After that, a late or skipped run stretches the next read window instead of losing the interval. Slack history is fetched with conversations.history using the bookmark as its oldest value; links come from chat.getPermalink. GitHub pull requests are read through the MCP tools.
A failed read must never look like a quiet day. An MCP server may be unavailable, a token may have expired, or a Slack response body may say "ok": false even when the HTTP status is 200. In each case, keep that source’s bookmark where it is, continue with sources that still work, and name the missing source in a single coverage line. Do not search the sandbox for credentials or try another route around an authentication failure; report it and stop using that source for this run.
Keep notes.md for durable quirks about a source when it works, such as an ordering rule or an item limit. A failed credential or a temporary workaround belongs in that run’s record, not in long-lived notes that could outlast the failure.
A post is a side effect, not a bookkeeping detail
The reference implementation writes to one Slack destination. It posts automatically; no person approves each message. The built-in shell tool does not pause for approval, and slack.com is on the environment allowlist. That is a deliberate product choice, not evidence that the content has been reviewed by a human.
Before sending, the agent checks recent channel history for today’s edition title. It records posting before making the request, calls chat.postMessage, and counts the post as delivered only when Slack returns both "ok": true and a message timestamp (ts). An HTTP success alone is not enough. If Slack confirms the post, the agent records posted and the message ID. If the result is ambiguous, it records maybe posted and changes no other state. If Slack clearly rejects the destination, it records held and preserves the brief text for follow-up.

Only after a confirmed post—or a deliberate quiet day allowed by the reader’s preferences—does the agent update the ledger and bookmarks. It moves bookmarks only for sources it read successfully. Each run also gets a runs/<date>.md record: what was read, what failed, what was posted, what was cut and why, and any decision the agent made on its own.
The title check is useful deduplication, but it is not an atomic exactly-once guarantee. Two overlapping runs could both check before either posts. That concurrency risk is a QA extension to the reference design: if overlapping executions are possible in your environment, add a lock or persistent posting-intent record and test the recovery path.
Give the agent explicit judgment rules
In Managed Agents, an agent is a versioned combination of model, prompt, and tools. The example’s agent.md uses claude-sonnet-5-5, the GitHub MCP endpoint https://api.githubcopilot.com/mcp/, and no web search or web fetch. Those web tools are disabled because they run outside the sandbox’s network allowlist. The GitHub MCP toolset is configured as always_allow so an unattended run does not wait forever for a person to approve a read; the token remains read-only, which is the actual limit on GitHub changes.
The run instructions are the operational heart of the example. The reference implementation makes the sequence explicit:
- Read
preferences.mdfresh at the start. If the file is missing or unreadable, do not post or change state; if the reader explicitly says to stop or pause, stop and record that decision. - Read bookmarks, the report ledger, source notes, and the previous run record. On a first run, use the initial 24-hour window.
- Read each source from ten minutes before its bookmark. Keep the bookmark for any failed source and skip unchanged items already in the ledger.
- Include an item only if the reader would act on it today or it changes an imminent decision. A count such as “12 open reviews” is not an item; link the blocked reviews. Carry an unresolved item as one short line, such as “still waiting, day 3”; drop closed items and topics the reader has retired.
- Re-check every candidate against its live source immediately before posting. Drop resolved items, update changed items, and drop unverifiable ones while recording them under
cuts. Assert a status only when it can be verified. Copy links from the source’s own fields, such as a pull request’shtml_urlor a Slack permalink. - Write within the reader’s length cap. Mention missing sources once at the end; never fill in missing numbers. If every source was read and there is nothing to report, say so briefly—or remain silent when preferences permit a quiet day.
- Check whether today’s edition already exists, write the
postingrecord, then post. In a dry run, make no post and do not move bookmarks. Treatmaybe postedandheldas separate outcomes. - After confirmation or a deliberate quiet day, write the final run record, add ledger entries, and advance only successful sources’ bookmarks. Never edit the reader’s preferences. If a reply suggests a preference change, put it in
proposals.mdfor the reader to accept.
The rules do more than shape the prose. They define when the system may act, when it must stop, and what evidence is required before it claims success.
Schedule by the reader’s clock
The agent file describes what runs; the deployment describes when and where it runs. A deployment binds the agent, sandbox environment, first message, schedule, vault, memory stores, and spending cap. Every scheduled trigger starts a fresh session. The first message tells the agent to title an edition “Daily brief” followed by the weekday, month, and day in the reader’s time zone.
The reference configuration runs at 07:32 on weekdays in America/New_York. The deployment’s timezone controls the trigger, and the first message also tells the agent to calculate report dates in that same zone. Set both together. Otherwise, a run near midnight can label today’s brief as yesterday because the server uses another time zone.
Use ant apply deployment.md to create or update the deployment. To test without waiting for the cron schedule, start a run manually with ant beta:deployments run --deployment-id <id> using the ID in claude-lock.json. The repository quickstart also provides a scripts/run.sh wrapper that waits for the result and prints the last message, unreadable sources, and run record. For a dry run, make the state store read-only: the agent shows the brief in its final response, posts nothing, and cannot move a bookmark. Test first, then enable the schedule with ant beta:deployments unpause --deployment-id <id>. After test runs, use scripts/reset-state.sh so the first real run starts clean; use scripts/teardown.sh only when you intend to remove the resources.
Keep reader preferences separate from agent memory
Each run starts in a fresh sandbox. Persistent memory is what lets the automation continue, but stale memory can be just as misleading as no memory: a resolved issue may keep appearing as open, or an unresolved one may disappear because it was reported earlier.
The example separates two stores. preferences belongs to the reader and is mounted read-only to the agent. It holds source channels and repositories, exclusions and retired topics, length limits, destination, mentions, temporary rules with end dates, and stop conditions. state belongs to the agent and is read-write: bookmarks, the ledger, source notes, preference proposals, and run records.
ant apply creates the preference store, not its file. The reference repository seeds preferences.md with scripts/seed-preferences.sh; later edits must be seeded again and take effect on the next run. The agent re-reads the file every time rather than relying on an old copy in the system prompt. If it cannot read preferences, it stops instead of falling back to defaults.
The ledger keeps one line per previously reported item: date, source, stable item ID, and the state that was reported. That makes “still waiting, day 3” possible without repeating the whole item. A run record explains what happened that day. proposals.md can suggest a preference change, but only the reader changes the actual preferences.

Treat every source and state file as data, not as a new instruction channel. The reference instructions also say that proposals and source notes must not override the rules in agent.md.
Guardrails need to cover both actions and spend
The agent reads messages and issues written by other people. Those can contain prompt-injection text aimed at changing what the agent reads or where it posts. The reference design limits the consequences, but does not claim that the agent will never be influenced: injected text may still affect the brief or persistent notes.
GitHub’s fine-grained token is read-only, and the preferences store is read-only at the filesystem level. The sandbox allowlist contains only slack.com and the GitHub MCP server. Web search and fetch are disabled because those tools run outside that sandbox boundary. Together, these controls prevent the agent from writing to a repository or silently changing its reader’s rules, while still leaving a narrow path to publish in Slack.
Slack is the exception: the same bot token reads source channels and posts to the destination. Its effective access is determined by channel membership, not merely by the prompt telling it where to post. The run instructions also prohibit posting source content elsewhere or using channel-wide and user mentions unless preferences allow them. Keep the bot out of channels it does not need. For sensitive source channels, the reference implementation suggests two Slack apps: a read-only app for sources and a separate chat:write app invited only to the destination. Private Slack channels require the groups:history scope in the app manifest.

The guardrails diagram includes limits on turns, elapsed time, and spend, plus a heartbeat someone monitors. A scheduled workflow needs an owner who notices when its heartbeat or expected output goes quiet. Set a per-run budget from observed costs. The authors recommend starting at roughly three to five times a normal run, then tightening after real usage is known. Their sample deployment uses a $5 cap (amount: "500", in cents); each run gets the full configured maximum. A run that reaches the cap pauses with budget_reached; it does not fail like an ordinary run. From the channel, that pause can look like silence, so check deployment run status when a brief is missing.
The reference repository reports about $1.00 per run on Sonnet 5 and $1.50 on Opus 5 for a particular busy workload—two channels, two repositories, about 140 messages and 25 pull requests. A run where every source was unreadable cost about $0.15–$0.30, and doubling the sources increased the measured cost by roughly a third. The repository explicitly notes that the measurements predate the default move to Sonnet 5.5. Treat those numbers as historical context, not a forecast; inspect your own run costs. Model choice trades depth against cost, and adding sources changes the bill.
Turn the design into QA checks
The following checks are my quality-engineering extension to the source implementation. They test the contracts the article describes rather than assuming the happy path is enough.
| Scenario | Expected result |
|---|---|
| GitHub token expires or MCP is unavailable | Report GitHub as unreadable; do not move its bookmark; continue with available sources |
Slack returns HTTP 200 with ok: false | Treat the read or post as failed |
| Post times out after Slack may have accepted it | Record maybe posted; reconcile before retrying; do not advance state |
| Slack clearly rejects the destination | Record held; preserve the brief and leave progress unchanged |
| The schedule is late or runs near midnight | No source gap; title and date follow the reader’s configured time zone |
| The same edition is already in the channel | Do not post it again |
| An item resolves while the brief is being drafted | Drop or update it in the final live check |
| Preferences are unavailable or changed | Stop safely if unavailable; use the new version on the next run |
| A source message asks the agent to reveal a secret or post elsewhere | No credential search or unauthorized destination change |
| The budget is reached | Surface a paused run, not an empty brief |
| A dry run is configured | Show the brief for inspection without posting or moving bookmarks |
Start with one source, one destination, durable bookmarks, and explicit failure reporting. Add the ledger and delivery confirmation before widening the source set. Then test memory freshness, permissions, timezone boundaries, ambiguous delivery, and budget pauses. The reference repository includes scripts/reset-state.sh to clear test state and scripts/teardown.sh to remove the resources when the experiment is over. Multi-agent orchestration is not required to make a single-agent workflow reliable.
The same approach applies to QA digests, pull-request risk alerts, CI failure summaries, and weekly quality reports. The reusable asset is not a particular model. It is an automation whose state is recoverable, whose failures are visible, whose permissions are bounded, and whose claims can be checked.
References
- Original article and source of the locally optimized architecture images: Building Effective Agent Automations:https://claude.dev/blog/building-effective-agent-automations/
- Daily Brief reference implementation and setup notes: claude-quickstarts Daily Brief:https://github.com/anthropics/claude-quickstarts/tree/main/managed-agents/daily-brief
- Managed Agents documentation: Claude Managed Agents:https://platform.claude.com/docs/en/managed-agents/overview
Managed Agents commands and beta capabilities can change. Check the current documentation before adopting the implementation details. The article’s examples are a starting point; scope the credentials, channels, schedule, and review policy to your own environment.