DEV Community

Murthy G
Murthy G

Posted on Edited on

AWL: Stop AI Agents from Acting Before You Approve: Work Orders for TypeScript, Python and MCP

Structured Workflows for AI Agents: TypeScript, Python, and MCP

Update (October 2026), version 0.7: agents can now be stopped from sending emails, opening issues or issuing refunds until a manager approves their work. Jump to what's new.

AI agents can complete tasks, generate code, and collaborate with other agents. But as workflows become more complex, a question becomes increasingly important:

How do we make work between AI agents structured, traceable, and easier to hand off?

Passing messages between agents is straightforward. Keeping track of requirements, decisions, results, validation, and unfinished work is much harder.

I've been working on an open-source project to explore a structured approach to this problem.

The idea: Treat agent work as something structured

Imagine a research agent handing its findings to a coding agent.

The coding agent needs more than a summary. It may need the original requirements, constraints, decisions already made, results, unresolved questions, and the next steps.

Without a consistent format, every application ends up inventing its own conventions.

My project explores using Work Orders and Work Reports to represent work and its outcomes in a structured, reusable way.

One project, multiple integrations

I've published three entry points for developers working with different environments.

1. TypeScript / JavaScript

agent-work-layer

The core package provides the foundation for structured work management, including schemas, validation, lifecycle management, and work handoffs.

Install it:

npm install agent-work-layer
Enter fullscreen mode Exit fullscreen mode

2. MCP integration

agent-work-layer-mcp

The MCP package makes work-management operations available through the Model Context Protocol, so compatible AI clients (Claude Code, for example) can create, accept, report on and review work through MCP tools.

Run it, or install it:

npx -y agent-work-layer-mcp
npm install agent-work-layer-mcp
Enter fullscreen mode Exit fullscreen mode

It also includes an MCP gateway that sits between a worker agent and its real tools. The gateway enforces each work order's allowed tools and budget on every call and records each call as evidence. Since 0.7, it can also hold back writes until the work is approved (see below).

3. Python implementation

agent-work-layer on PyPI

For Python developers, the project also includes a Python implementation with the same document models and validation rules.

This gives Python-based AI-agent applications a way to explore the same broader approach to structured work, without requiring the entire application to be written in TypeScript.

pip install agent-work-layer
Enter fullscreen mode Exit fullscreen mode

Both implementations run the same shared test cases, so they report the same findings for the same documents.

What could this look like in practice?

Consider a multi-agent workflow:

  1. A coordinator defines a task and its constraints.
  2. A research agent investigates the problem.
  3. The agent records its results and unresolved questions.
  4. An implementation agent continues from the recorded work.
  5. Validation and lifecycle rules help track the outcome.
  6. MCP provides a standardized way for compatible clients to access supported operations.

The objective is to make agent handoffs less dependent on ad hoc prompts and more dependent on explicit, inspectable work structures.

New in 0.7: approve an agent's writes before they happen

Reviewing an agent's work after it finishes has a gap. By the time you reject the report, the emails are already sent, the GitHub issue is open and the refund has gone through. Rejecting the report changes none of that.

Version 0.7 adds staged tools. The manager lists the tools whose calls should wait for review:

import { WorkDesk } from "agent-work-layer";

const desk = new WorkDesk();
const { order } = await desk.create(
  {
    to: "agent:assistant",
    goal: "Email the CFO a three-line Q3 risk summary",
    criteria: [{ id: "c1", text: "Summary sent to the CFO" }],
    budget: { usd: 1, tool_calls: 10 },
    deadline: new Date(Date.now() + 86_400_000).toISOString(),
    tools: ["filings/*", "gmail/*"],
    staged: ["gmail/send"], // writes that wait for approval
  },
  "agent:manager",
);
Enter fullscreen mode Exit fullscreen mode

The worker reaches its real MCP servers through the gateway:

![Staged tools: the worker's read runs straight through the gateway, but its gmail/send call is held, logged, and answered with a placeholder. The worker lists it in its report; on approve, the gateway runs it once; on rework it is dropped]

  • Reads run normally. The worker can still research. Only the staged calls wait.
  • Writes are held. The gateway counts and logs a staged call with its arguments, but doesn't forward it. The worker gets back a placeholder such as pending:gmail/send#3 and lists the call in its report's effects.
  • The manager decides. Approve, and the gateway runs the held calls once, in order. Send the work back, and they're dropped. Since nothing ran, there's nothing to undo.
# the worker's tools, through the gateway
npx -y agent-work-layer-mcp gateway --order wo_123 --config upstream.json

# after the manager approves the report
npx -y agent-work-layer-mcp gateway --order wo_123 --config upstream.json --commit
Enter fullscreen mode Exit fullscreen mode

A few details turned out to matter once I tried this with real workflows:

  • Approve only some effects. The manager can approve the work but run only the effects they choose:
  await desk.review(order.id, "agent:manager", "approve", "CFO copy only", { effects: ["gmail/send#3"] });
Enter fullscreen mode Exit fullscreen mode
  • Effects can depend on each other. A worker can't know the number of an issue that hasn't been created yet. A later held call can refer to an earlier one's result, for example { "$ref": "github/create_issue#3", "pointer": "/structuredContent/number" }, and the gateway fills it in when the effects run.
  • Each effect runs at most once. Before running anything, the gateway checks every effect in the report against what it actually logged, so a worker can't show the manager one email and send another. If anything doesn't match, nothing runs. Running --commit again never sends anything twice, even after a crash.
  • Sub-agents can't get around it. A worker can't hand a staged tool to a sub-agent, because the sub-agent's writes would run when the worker approves them, before the manager has seen anything.

Two smaller additions in the same release:

  • Progress updates: a worker can post notes while it works (desk.progress(id, worker, "Drafted the summary", 50)), and anyone following the order is notified.
  • Track records: desk.trackRecord("agent:assistant") shows how a worker's orders turned out: approved the first time, sent back for rework, failed or expired, and what it spent against budget.

If you're upgrading, update the gateway along with the desk. Gateways older than 0.7.0 don't know about staged and forward those calls immediately.

Why I'm building this

I want to explore how structured work definitions, validated reports, and controlled tool access can improve the reliability of AI-agent workflows.

Rather than every project independently inventing its own work-order format, lifecycle rules, and handoff conventions, could these become reusable infrastructure?

That's the problem I'm working on.

Explore the project

I'd love your feedback

If you're building AI agents, agent frameworks, or MCP integrations:

  • How do you structure work passed between agents?
  • Do you use explicit task and result schemas?
  • How do you control tool access for individual tasks?
  • Do you let agents take real actions (send, pay, merge) without a human in the loop, or do you hold them for review?
  • Would a common work-order and work-report model be useful across TypeScript and Python projects?

I'm interested in feedback, contributions, and real-world use cases from developers working on these problems.

Top comments (11)

Collapse
 
suirindo profile image
asagiman •

The staged-effect design gives us a concrete crash boundary to test. What happens if the upstream service accepts an effect, then the gateway crashes before recording the response?

I read your 'at most once' statement as distinct from guaranteed completion: marking an effect dispatched before the call can prevent a second attempt, while leaving its outcome unknown. Retrying after that uncertainty needs receiver-side idempotency or reconciliation. Does 0.7 expose an explicit unknown-outcome state, and what evidence lets the manager resolve it?

A small loopback fixture could cover three cases separately: crash before dispatch; receiver accepts but response is lost; result durably recorded then --commit repeats. The receiver's delivery count would be independent of the gateway log. I'm suggesting a test shape here, not reporting a reproduced defect.

Collapse
 
murthyg profile image
Murthy G •

Thanks, you read it correctly: the guarantee is at most once, not that the effect completes. 0.8.0 recorded an interrupted effect as error, and a dropped connection or timeout counted as a failure too.

Fixed in agent-work-layer-mcp 0.8.1 (#43):

An effect that was sent but got no answer is now unknown, and it is never run again.
Each effect call carries _meta["awl/ref"] and _meta["awl/order"], so the receiver can dedupe and check what happened.
Your three cases are now tests, using a loopback receiver that counts deliveries on its own.
Follow-ups: #44 (stale commit lock after a crash) and #45 (letting a manager settle an unknown effect).

Collapse
 
suirindo profile image
asagiman •

Thanks for implementing the three cases. Keeping an unanswered send as unknown, with no automatic replay, makes the outcome boundary explicit.

Could you share the #43 PR and test-file links, ideally pinned to the tested commit? I’d like to record this as an external implementation response to the proposed cases, with your attribution and the remaining #44/#45 limits. I haven’t independently reproduced the tests.

Thread Thread
 
murthyg profile image
Murthy G •
Thread Thread
 
suirindo profile image
asagiman •

I independently reproduced the focused CLI + gateway suite from PR #43's head, 493157aee3391ec89fe1ffd301a402ae6e61861c (Linux x86_64, Node v24.22.1): 42 passed, 0 failed, 0 skipped. Commands after checkout: npm ci --ignore-scripts --no-audit --no-fund, npm run build -w agent-work-layer, then node --test packages/mcp/test/cli.test.ts packages/mcp/test/gateway.test.ts.

The separate stdio receiver records one delivery through gateway kill-after-receipt, lost response, and repeated commit after a recorded result. The crash/lost-response outcomes remain unknown and are not resent; the receiver also checks awl/ref and awl/order.

Two coverage limits matter: the killed-process test manually removes the stale lock before recovery (#44), and the before/after-send ambiguity is tested by injecting a running log record, rather than actually killing immediately before dispatch. This supports the bounded local fixture, not guaranteed completion or production receiver deduplication.

For #45, I'd keep evidence-based settlement separate from permission to retry: an absent receiver record alone may be incomplete evidence of non-execution, and a retry should recheck current authority while retaining the logical operation identity. Thanks for making the changes and tests inspectable.

Thread Thread
 
murthyg profile image
Murthy G •

Thanks for reproducing it independently. Agreed on both limits: the stale lock is #44, and the before/after-send case is only simulated through the log, which is the boundary the fixture is meant to cover. I'm not claiming guaranteed completion or that real receivers dedupe.

Good points for #45. I'll keep recording what happened separate from permission to retry, make a retry recheck current authority, and keep the same awl/ref across retries. I'll add them to the issue.

Thread Thread
 
suirindo profile image
asagiman •

Thanks for the precise boundary and the #43 update. The key comparison for me is whether a delivered effect with a lost answer remains UNKNOWN without a second delivery, and whether settling that unknown preserves the original awl/ref while rechecking current authority. Your loopback receiver counts are useful evidence for the fixture; they do not establish deduplication by an arbitrary receiver. I'll keep #44's stale lock and #45's settlement path separate when comparing this with ARC's fixed local lab.

Collapse
 
raju_dandigam profile image
Raju Dandigam •

The Work Order / Work Report split is a useful handoff contract, especially if the MCP gateway can bind tool permissions to a specific order. I'd keep an immutable order ID and policy revision on each tool-call evidence record; otherwise a resumed agent may inherit a report without knowing which authority was in force when an action ran. Do you model retries and handoffs as new report revisions, or append events to one execution history?

Collapse
 
murthyg profile image
Murthy G •

Good question. It's one execution history per order, with nothing replaced:

The order never changes after it's created. The desk has no edit operation. The order id is therefore the policy revision: anything that ran under wo_1 ran under exactly those tools, budget and deadline.
Retries add to the same order. When a manager sends work back for rework, the worker's next report is added with an incremented attempt. Every transition is an event with an increasing seq, and earlier reports stay in the record. The gateway numbers tool calls per order across reworks (filings/search#7), and those refs are what the report cites as evidence.
A handoff to a different worker is a new order. The manager can copy the previous worker's handoff (decisions, what was tried, open questions) into it. The spec treats a handoff as the previous worker's account, not verified fact.
You've found a real gap, though, and I've opened two follow-ups:

A call record knows its order only because the log is one file per order. Each record will carry its order id, the report attempt, and the id of the signed Delegation Pass it ran under, so evidence stays tied to the authority in force when it's carried across a resume: github.com/probita-ai/agent-work-l...

A reissued order has no formal link back to the one it continues. parent_id covers sub-orders, not "this replaces wo_1". A new continues field will let a resumed agent trace which order, and so which authority, applied to each earlier action:

github.com/probita-ai/agent-work-l...

Would those have covered your case? Feedback on either issue is welcome.

Collapse
 
murthyg profile image
Murthy G •

Update: both follow-ups are fixed and released in agent-work-layer 0.5.0.

Every gateway call record now carries its order_id, report attempt, and the jti of the signed Delegation Pass it ran under (#17).
A reissued order can set continues to link back to the finished order it replaces, so you can trace the whole chain (#19).

Collapse
 
murthyg profile image
Murthy G •