In my previous post, we looked at Evaluation and Monitoring — how to measure whether an agent is actually doing a good job. But measuring quality assumes the agent is working on the right things in the right order. What happens when it isn’t?
Give a capable agent a tool belt and a backlog and it will happily start working. The problem is that “working” and “working on what matters” are not the same thing. When five tasks arrive at once and three of them claim to be urgent, a naive agent just processes them in arrival order — or worse, in whatever order they happened to land in the prompt.
That’s where the Prioritization pattern comes in. It’s the difference between an agent that executes and an agent that triages.
Pattern #20: Prioritization#
The Problem#
Real workloads don’t arrive as a clean, ordered queue. They arrive as a mess: a “fix the database bug, it’s urgent!” sitting next to a “update the docs sometime” sitting next to a “investigate that flaky test.” An agent that treats all of these as equal — first in, first out — burns its budget, its tool calls, and your money on low-value work while the thing that’s actually on fire waits its turn.
Worse, urgency is often claimed rather than real. Every stakeholder thinks their request is a P0. An agent with no internal model of priority will either trust every “urgent!” at face value, or ignore them all. Neither is good triage.
Prioritization is the pattern that gives the agent a deliberate ranking step: before it acts, it assigns each task a priority level, decides who (or what) handles it, and then works the list in order of importance — not order of arrival.
The Solution#
The cleanest way to model this is a Project Manager agent: a ReAct agent that owns a small task-management system and a set of tools for creating, prioritizing, and assigning work. The agent reasons about each incoming request, then calls the right tool to record its decision.
Start with the data model — a Task with an explicit, optional priority field, and an in-memory manager that owns the lifecycle:
class Task(BaseModel):
"""Represents a single task in the system."""
id: str
description: str
priority: Optional[str] = None # P0, P1, P2
assigned_to: Optional[str] = None # Name of the worker
class TaskManager:
"""Manages tasks in memory."""
def __init__(self):
self.tasks: Dict[str, Task] = {}
self.next_id = 1
def create(self, description: str) -> Task:
task_id = f"TASK-{self.next_id:03d}"
task = Task(id=task_id, description=description)
self.tasks[task_id] = task
self.next_id += 1
return task
def update(self, task_id: str, priority=None, assigned_to=None) -> Optional[Task]:
if task_id in self.tasks:
task = self.tasks[task_id]
if priority: task.priority = priority
if assigned_to: task.assigned_to = assigned_to
return task
return NoneThe key design choice is that priority is a first-class field, not a guess the agent re-derives every time. Once the agent decides TASK-001 is a P0, that decision is persisted in state. Prioritization becomes a recorded fact, not a vibe that evaporates between turns.
Next, expose the operations as discrete tools. Note the separation: creating a task, setting its priority, and assigning a worker are three distinct actions. The agent has to make three explicit decisions, and each one shows up in the trace:
def create_task_tool(description: str) -> str:
"""Creates a new task and returns its ID."""
t = task_manager.create(description)
return f"Created task {t.id}."
def prioritize_task_tool(task_id: str, priority: str) -> str:
"""Sets priority for a task (P0, P1, P2)."""
t = task_manager.update(task_id, priority=priority)
return f"Priority {priority} set for {task_id}." if t else "Task not found."
def assign_task_tool(task_id: str, worker: str) -> str:
"""Assigns a task to a worker."""
t = task_manager.update(task_id, assigned_to=worker)
return f"Assigned {task_id} to {worker}." if t else "Task not found."Then wire it into a ReAct agent. The prioritization policy lives in the system prompt — and crucially, so does a sensible default so the agent never stalls on missing information:
def setup_pm_agent():
llm = get_llm(temperature=0)
tools = [
Tool(name="create_task", func=create_task_tool, description="Create a task. Input: description"),
Tool(name="prioritize", func=prioritize_task_tool, description="Set priority. Input: 'TASK-ID, PRIORITY'"),
Tool(name="assign", func=assign_task_tool, description="Assign worker. Input: 'TASK-ID, WORKER'"),
Tool(name="list", func=task_manager.list_tasks, description="List all tasks.")
]
prompt = ChatPromptTemplate.from_messages([
("system", "You are a Project Manager agent. Create tasks, then assign priority and workers. Default to P1 and 'Worker A' if unspecified."),
("placeholder", "{chat_history}"),
("human", "{input}"),
("placeholder", "{agent_scratchpad}")
])
agent = create_react_agent(llm, tools, prompt)
return AgentExecutor(
agent=agent,
tools=tools,
verbose=True,
memory=ConversationBufferMemory(memory_key="chat_history", return_messages=True)
)Three things make this work as a prioritization pattern and not just a CRUD wrapper:
- An explicit priority scale (
P0, P1, P2). The agent isn’t ranking on a vague continuum — it’s mapping each task onto a small, well-understood ladder. P0 means drop everything; P2 means whenever. - A default policy (
Default to P1 and 'Worker A' if unspecified). This is the unsung hero. Without it, an underspecified request like “look into the logs” leaves the agent paralyzed or hallucinating a priority. With it, the agent has a safe fallback and keeps moving. - Conversational memory (
ConversationBufferMemory). Prioritization is rarely one-shot. New tasks arrive, urgencies change, a P1 gets bumped to P0. Memory lets the agent re-rank the existing backlog instead of treating every message as a fresh world.
When you feed this agent “Create a task to fix the database bug, it’s urgent!”, the ReAct loop does the work out loud: it calls create_task, reasons that “urgent” maps to P0, calls prioritize with P0, and assigns it. The priority decision is now a visible, auditable step in the trace — not an implicit ordering buried inside the model’s head.
Why This Matters#
Prioritization is what turns a reactive tool-caller into something that behaves like an operator. A few places it earns its keep:
- Support and ticketing agents: triage incoming tickets by severity before routing them, so the P0 outage doesn’t sit behind a password-reset request.
- Autonomous PM / ops agents: maintain a live backlog, re-rank as conditions change, and assign work to the right human or sub-agent.
- Resource-constrained execution: when you have a token budget or a rate limit (see Resource-Aware Optimization), prioritization decides which work gets the expensive model and which gets the cheap one.
- Multi-goal agents: when several goals compete for the same next action, an explicit priority field breaks the tie deterministically instead of leaving it to sampling noise.
The trade-offs are real and worth stating honestly:
- The policy is only as good as the prompt. “Default to P1” is a blunt instrument. For serious systems you’ll want the priority scale and the assignment rules to come from a structured policy — config or a rules engine — not a single sentence the model can drift away from.
temperature=0is doing heavy lifting here. Prioritization needs to be deterministic and defensible. If the same backlog yields a different ranking on every run, nobody will trust the agent. Keep the ranking step low-temperature.- In-memory state doesn’t survive a restart. The
TaskManagerhere is a teaching scaffold. In production the backlog lives in a database or a real queue, and the agent’s tools talk to that — otherwise your carefully-ranked priorities vanish on the next deploy. - Claimed urgency is an attack surface. If “it’s urgent!” always becomes a P0, every requester learns to say it’s urgent. A mature prioritization agent weighs the claim against evidence rather than rubber-stamping it.
Rule of thumb: reach for this pattern the moment your agent has more than one thing it could do next and the order matters. If your agent only ever has a single obvious next action, you don’t need prioritization — you need it precisely when the backlog grows faster than the agent can clear it.
The Bigger Picture#
This is post #20 in my series documenting Antonio Gulli’s Agentic Design Patterns. All credit for the conceptual framework goes to him — I’m focused on producing clean, runnable Python implementations you can clone, modify, and ship.
Prioritization sits downstream of nearly everything else in the series. Planning decides what steps exist; Prioritization decides which step goes first. Goal Setting tells the agent what to optimize for; Prioritization resolves the conflicts when goals compete. It’s a small pattern conceptually, but it’s the one that separates an agent that does work from an agent you’d actually trust to manage a queue.
All the code from this post is in my repository: carlosprados/Agentic_Design_Patterns, specifically under 20_Prioritization/. The PM agent runs with uv run, supports Gemini and Ollama via the shared get_llm() abstraction, and is ready to fork.
What’s Next#
This was the penultimate pattern. In the final post of the series we’ll tackle Exploration and Discovery — the pattern where an agent stops merely executing known tasks and starts probing the unknown: generating hypotheses, running experiments, and discovering options it was never explicitly told to consider. It’s the pattern that turns an agent from a worker into a researcher.
One chapter left. Stay tuned.

