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.

The Daily Brief architecture: a schedule starts an agent, which reads sources, consults separate preferences and state, and posts to one destination within guardrails

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 resourceWhat it controls
agent.mdModel, tools, and the instructions for each run
deployment.mdSchedule, time zone, first message, budget, vault, and memory stores
environment.yamlWhich network hosts the sandbox may reach
memory_store_preferences.yamlReader-owned settings, mounted read-only
memory_store_state.yamlBookmarks, ledger, notes, proposals, and run records
vault.yamlThe credential vault attached to the deployment
slack/manifest.yamlThe Slack app and its scopes
claude-lock.jsonResource 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.

The source component highlighted in the Daily Brief architecture: Slack and GitHub are read-only inputs reached with vault-managed credentials

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.

The destination component highlighted in the Daily Brief architecture: the agent checks for an existing edition before writing to its one approved Slack channel

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:

  1. Read preferences.md fresh 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.
  2. Read bookmarks, the report ledger, source notes, and the previous run record. On a first run, use the initial 24-hour window.
  3. Read each source from ten minutes before its bookmark. Keep the bookmark for any failed source and skip unchanged items already in the ledger.
  4. 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.
  5. 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’s html_url or a Slack permalink.
  6. 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.
  7. Check whether today’s edition already exists, write the posting record, then post. In a dry run, make no post and do not move bookmarks. Treat maybe posted and held as separate outcomes.
  8. 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.md for 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.

The memory component highlighted in the Daily Brief architecture: the reader owns read-only preferences while the agent writes operational state

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 component highlighted in the Daily Brief architecture: network access, tool permissions, and per-run spending are bounded

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.

ScenarioExpected result
GitHub token expires or MCP is unavailableReport GitHub as unreadable; do not move its bookmark; continue with available sources
Slack returns HTTP 200 with ok: falseTreat the read or post as failed
Post times out after Slack may have accepted itRecord maybe posted; reconcile before retrying; do not advance state
Slack clearly rejects the destinationRecord held; preserve the brief and leave progress unchanged
The schedule is late or runs near midnightNo source gap; title and date follow the reader’s configured time zone
The same edition is already in the channelDo not post it again
An item resolves while the brief is being draftedDrop or update it in the final live check
Preferences are unavailable or changedStop safely if unavailable; use the new version on the next run
A source message asks the agent to reveal a secret or post elsewhereNo credential search or unauthorized destination change
The budget is reachedSurface a paused run, not an empty brief
A dry run is configuredShow 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

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.

Share