Skip to main content

Agentic Patterns II: Decision Workers

· 8 min read
Colt McNealy
Founder & Managing Member

How can we mitigate issues like prompt injection, governance, and plain old integration challenges when using agents for automation? We've helped our customers overcome these problems with the Decision Workers pattern, which separates the concerns of intelligence and orchestration. In this pattern, LittleHorse WfSpecs achieve reliable and secure orchestration while Decision Workers (Agents with read-only tool access) provide the intelligence.

info

This is the second part in a four-part blog series about building agentic workflows with LittleHorse. This blog series will help you automate internal and customer-facing business processes with guardrails and security.

  1. Safe Agentic Decisions
  2. [This Post] Decision Workers
  3. [Coming Soon] Verified Agents
  4. [Coming Soon] Workflow Dispatch Agents

Example Decision Worker Workflow

In this pattern, a LittleHorse workflow (WfSpec) consults an agent to extract important information and make a business decision about which path to take in the process. The agent does not orchestrate the process itself but rather serves as a router which decides what path to take. Along the way, deterministic guardrails enforce security and catch erroneous decisions that agents make, and the orchestrator (here, a WfSpec) shepherds the process to completion while collecting an audit trail of everything that happened.

The key to making the agent inside the Decision Worker effective yet safe is to give it tools that have read-only access to sensitive systems. The agent is free to fetch as much context as it needs in order to make a decision; however, it does not directly take action itself but rather outputs a structured decision which is then acted upon by the WfSpec.

Case Study: Ticket Triage

Let's analyze two architectures for a system that automatically triages incoming support requests over email. We'll look at the system with two architectures:

  1. Giving the agent full responsibility for orchestrating the process.
  2. Using Decision Workers to limit the responsibility of the agent.

Our system will use an agent to read an email, fetch context using tools (eg. find info about the order, find info about the user, etc), and then decide how to handle the ticket:

  1. Escalate to a human.
  2. Send info about the order to the user.
  3. Cancel the order and issue a refund.

Naive Agent-Driven Orchestration

A naive approach would be to give an agent multiple tools: some tools to fetch context (such as looking up information about the order and the user), and some tools to take action by hitting other API's. It would be dangerous to give the agent a tool such as:

processRefundAndCancelOrder(String orderId, BigDecimal refundAmount);

The services and API's involved in processing refunds are decidedly not multi-tenant, so this approach requires the agent to correctly propagate tenancy context. This causes the agent to become vulnerable to prompt injection attacks: a malicious email could manipulate the model into issuing an unauthorized action (such as canceling someone else's order).

Our problem is that the agent's decision space is not properly constrained: we're giving the agent the ability to decide to send a refund to the wrong person. In a multi-tenant system, that means the model must correctly preserve tenant context on every tool call. A malicious email can exploit that requirement through prompt injection, causing the system to cancel the wrong order or issue the wrong refund.

Agents perform far more reliably when you give them read access to tools, and then parse and act upon a structured output decision. But how can we effectively orchestrate this?

Decision Workers

As mentioned earlier, a Decision Worker is a pattern in which you wrap an agent inside a LittleHorse Task Worker. The agent has read-only tool access to sensitive systems, and outputs a decision which is acted upon by the WfSpec. As we'll see later, this pattern vastly mitigates the exposure to prompt injection attacks (or even simple hallucinations).

Here's a LittleHorse Task Worker which implements that:

public enum SupportTicketResponse {
ESCALATE_TO_TEAM,
SEND_ORDER_INFO,
CANCEL_ORDER
}

@LHTaskMethod("triage-support-ticket")
public SupportTicketResponse triageTicket(String emailContent) {

// Not shown here for simplicity: use the LLM in a loop to gather more context
// like a proper agent does.
//
// The agent is ONLY allowed to READ, not write.

String response = anthropic.messages().create(MessageCreateParams.builder()
.model("claude-sonnet-4-20250514")
.maxTokens(50)
.system("You are a support ticket classifier. Respond with exactly one of: " +
"ESCALATE_TO_TEAM, SEND_ORDER_INFO, CANCEL_ORDER. " +
"No other text.")
.addUserMessage(emailContent)
.build()
).content().get(0).text().trim();

try {
return SupportTicketResponse.valueOf(response);
} catch(Exception exn) {
return SupportTicketResponse.ESCALATE_TO_TEAM;
}
}
tip

Agents can take their time. Best to leave at least 60 seconds task timeout.

Below is a WfSpec which places guardrails around the above Decision Worker, handling all orchestration and enforcing proper security contexts.

@LHWorkflow("handle-support-ticket")
public void supportTicketFlow(WorkflowThread wf) {
WfRunVariable emailBody = wf.declareStr("email-body").required();
WfRunVariable status = wf.declareStr("status").searchable();
WfRunVariable userId = wf.declareStr("user-id").searchable();
WfRunVariable orderId = wf.declareStr("order-id").searchable();

status.assign("TRIAGING");

// Fail and escalate to humans if the agent cannot extract a valid order-id which
// belongs to the user
orderId.assign(wf.execute("extract-order-id", emailBody, userId));
wf.doIf(wf.execute("validate-order-and-user", userId, orderId).not(), handler -> {
handler.assignUserTask("review-support-ticket", null, "support-team").withNotes("couldn't extract valid orderId");
});

// Decision Agent here!!!
WfRunVariable decision = wf.declareStr("decision");
decision.assign(wf.execute("triage-support-ticket", emailBody));

wf.doIf(decision.isEqualTo("CANCEL_ORDER"), then -> {
status.assign("REFUNDING");
// Avoid prompt injection: we know the orderId is tied to the user. This is
// controlled by the workflow, not the agent.
then.execute("process-refund", userId, orderId);
then.execute("cancel-order", userId, orderId);
then.execute("send-email", userId, "Refund Issued", "We've issued a refund for your recent order.");

}).doElseIf(decision.isEqualTo("SEND_ORDER_INFO"), then -> {
status.assign("RESPONDING");
then.execute("send-order-info", orderId, userId);

}).doElseIf(decision.isEqualTo("ESCALATE_TO_TEAM"), then -> {
status.assign("ESCALATED");
then.assignUserTask("review-support-ticket", null, "support-team").withNotes(orderId);

});

status.assign("DONE");
}

An Agentic Harness

A WfSpec is the perfect way to capture the power of an agent while reducing the amount of work it must do. It encodes processes which don't change (such as payments, onboarding, orders, etc) while freeing up agents to make decisions. Most importantly, using WfSpecs to harness your agents allows you to deterministically control what your agent does and doesn't touch.

Least Privilege

With the Decision Worker pattern, your agent only has to be able to find information rather than take (potentially destructive) actions. For example, our email triage worker might be allowed to look up any information it wants about the user, past orders, etc, but it's not able to (nor does it need to) call any privileged API's with write access.

Avoiding Prompt Injection

Crucially, the Decision Worker pattern makes it much harder to trick agents into spilling secrets. This is because the agent itself does not have the tools needed to do so: we can write non-agentic (deterministic) code to explicitly validate that the order we are acting on belongs to the user who sent the initial email. Even with malicious emails, the WfSpec deterministically enforces security constraints and takes away the agent's ability to make errors (for example, canceling the wrong user's order).

If you simply pass over the orchestration to the agent itself, you get no such defense.

Governance and Durability

Lastly, all of the traditional benefits of LittleHorse still apply here. You can see the execution history of all support tickets. LittleHorse still provides all of the durability and orchestration benefits between steps that it always does:

  • Audit trails of every step executed in every process, most importantly what decisions were made by the agents and when.
  • Useful debugging logs for any stuck or failed processes.
  • Ability to easily escalate to human intervention with UserTasks.

Most importantly, you can clearly see what decisions were made by which agent, and when.

Wrapping Up

Decision Workers are a pragmatic way to put agents into production: let the agent do what it is good at (interpreting messy human input and choosing intent), while the WfSpec does what deterministic software is good at (validation, side effects, retries, and escalation).

In our ticket-triage example, this separation gives you a safer architecture. The agent never gets direct authority to perform privileged write actions. Instead, the WfSpec validates identity and order ownership, then executes the approved path with a full audit trail.

Stay tuned for the next post about using agents to verify other agents!