4.8K
Orchestration & Control emerging

Classify-Then-Act for Background Agents

By James Ross (@jimy-r)
Add to Pack
or

Saved locally in this browser for now.

Cite This Pattern
APA
James Ross (@jimy-r) (2026). Classify-Then-Act for Background Agents. In *Awesome Agentic Patterns*. Retrieved September 26, 2026, from https://agentic-patterns.com/patterns/classify-then-act-for-background-agents
BibTeX
@misc{agentic_patterns_classify-then-act-for-background-agents,
  title = {Classify-Then-Act for Background Agents},
  author = {James Ross (@jimy-r)},
  year = {2026},
  howpublished = {\url{https://agentic-patterns.com/patterns/classify-then-act-for-background-agents}},
  note = {Awesome Agentic Patterns}
}
01

Problem

A background agent working through a task list has two failure modes at the extremes. Act on everything with full confidence and it ships work nobody wanted. Ask before doing anything and it becomes a nag that stalls on every ambiguous item, until the human stops trusting it to do anything alone.

02

Solution

Classify every incoming task into exactly one of three buckets before doing anything else. has-default means an obviously correct action exists, needs-intent means genuinely ambiguous and a human call, and out-of-scope means not the agent's job. Only has-default work gets built, and even then it is built speculatively in a sandbox and lodged in a review queue rather than applied directly, so the human approves before anything lands. Every rejection is written back as a short decision record (task, what was attempted, why it was rejected, the lesson for next time) that the agent greps before classifying anything that looks similar again. Three or more matched rejections force the classification down to needs-intent rather than a fourth speculative attempt at the same shape.

def classify(task):
    if grep(rejection_log, task.slug_or_keywords).count >= 3:
        bucket = NEEDS_INTENT  # still surface the task; do not silently return
    else:
        bucket = triage(task)  # has-default | needs-intent | out-of-scope
    if bucket not in {HAS_DEFAULT, NEEDS_INTENT, OUT_OF_SCOPE}:
        bucket = NEEDS_INTENT  # malformed, unknown, or failed triage needs review
    if bucket == HAS_DEFAULT:
        build_in_sandbox(task)
        lodge_for_review(task)
    elif bucket == NEEDS_INTENT:
        surface_for_human_decision(task)
    return bucket

Only tasks within an explicit human-delegated mandate enter this loop. A has-default label cannot authorize external side effects: sandbox builds still need bounded credentials, spending and network permissions. Surface needs-intent items on the operator’s normal review path, with a durable pending record, rather than blocking unrelated ready tasks.

03

How to use it

Worth adopting once a background or scheduled agent handles more than a handful of task shapes and full autonomy is too risky to grant outright. The three-way split only works if has-default stays genuinely conservative. Reserve it for actions a competent human would approve near-automatically, and route anything with real judgment content to needs-intent. The rejection log needs a consistent block format (task, attempt, reason, lesson) and a grep step wired into classification, rather than a file nobody reads back.

04

Trade-offs

  • Pros: avoids both failure extremes of pure autonomy and pure question-asking; the sandbox-then-review step means a wrong classification costs a discarded draft, not a live mistake; the rejection log stops the same bad idea being re-proposed on a fixed schedule.
  • Cons: this pattern only classifies work that already reached the agent; it says nothing about how the agent decided a task belonged to it in the first place. In the source workspace, the surrounding self-discovery loop, a scheduled agent scanning its own task list and inferring intent, is exactly what failed. needs-intent questions landed in a tracker file nobody read on their normal path, thirteen piled up unanswered, and each one stalled the task behind it. Separately, the unattended runtime's credential expired silently and roughly five weeks of cycles ran dark. Both failures sat in the authorization and discovery layer around the classifier, not in the classify-then-act logic itself, and the fix was to replace self-discovery with an explicit human-delegated queue while keeping the classifier, the sandbox build, and the rejection log unchanged. Treat this pattern as the inner loop of a system whose outer authorization boundary still needs a separate, deliberate answer.
06

References