> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentium.in/llms.txt
> Use this file to discover all available pages before exploring further.

# Gate a workflow with approval

> Run the same workflow with a denied and approved action through @agentium/harness.

Build a refund-review workflow with a read step and an approval-gated write step. The write only creates a simulated receipt in memory; it never contacts a payment service. Compare both outcomes before connecting a real application action.

| You will use | Requirement |
| - | - |
| Packages | `@agentium/core`, `@agentium/harness`, `zod` |
| Runtime | Node 22.18+ within v22, or 24.11+ within v24 |
| Services and keys | None; no model or external service |
| Result | A denied action performs zero submissions; approval permits one |

## Get the project

[Download the complete TypeScript project](/downloads/harness-workflow.zip), extract it, and run `npm install` in its directory. The archive includes this source, `package.json`, `tsconfig.json`, and a README.

To start manually in an empty directory:

```bash theme={null}
npm init -y
npm pkg set type=module
npm install @agentium/core@4.0.0 @agentium/harness@4.0.0 zod
npm install --save-dev typescript tsx @types/node
```

## Follow the execution

```mermaid theme={null}
flowchart LR
  I[Validate order input] --> R[Read eligibility]
  R --> P[Host execution policy asks for approval]
  P -->|Deny| D[No submission]
  P -->|Approve| W[Simulated refund receipt]
```

The Workflow defines the sequence. `workflowDriver` connects its steps to the harness. The runtime grants only the two named steps; an execution policy requires approval specifically for submission. The host supplies the decision.

Save as `harness-workflow.ts`:

```typescript harness-workflow.ts theme={null}
import { pathToFileURL } from "node:url";
import { ApprovalManager, Workflow } from "@agentium/core";
import { HarnessRuntime, workflowDriver } from "@agentium/harness";
import { z } from "zod";

const orderInput = z.object({ orderId: z.literal("order-42") }).strict();
type State = { orderId: string; eligible: boolean; receipt: string };

export async function reviewRefund(approved: boolean) {
  let submissions = 0;
  let decisions = 0;
  const approvals = new ApprovalManager({
    policy: "none", // Mandatory asks below come from the execution policy.
    onApproval: async (request) => {
      decisions++;
      console.log("approval:", request.toolName, approved ? "approved" : "denied");
      return { approved, reason: "Local demonstration decision" };
    },
  });
  const workflow = new Workflow<State>({
    name: "refund-review", register: false,
    initialState: { orderId: "", eligible: false, receipt: "" },
    steps: [
      {
        name: "check-order",
        run: async (state) => ({ eligible: state.orderId === "order-42" }),
      },
      {
        name: "submit-refund",
        run: async (state) => {
          if (!state.eligible) throw new Error("Order is not eligible");
          submissions++;
          // Local effect only. Replace with an authorized, idempotent service.
          return { receipt: `simulated-refund:${state.orderId}` };
        },
      },
    ],
  });
  const runtime = new HarnessRuntime({
    driver: workflowDriver(workflow, {
      input: (input) => {
        if (typeof input !== "string") throw new Error("Expected JSON text");
        return orderInput.parse(JSON.parse(input));
      },
    }),
    grants: { toolIds: ["workflow:check-order", "workflow:submit-refund"], modelRoles: [] },
    budgets: { maxModelCalls: 0 },
    approvalManager: approvals,
    executionPolicy: {
      decide: (request) => ({ action: request.toolName === "workflow:submit-refund" ? "ask" : "allow" }),
      resolveEffect: (request) => request.toolName === "workflow:submit-refund" ? "write" : "read",
    },
  });
  const identity = { tenantId: "demo", userId: "reviewer" };
  const sessionId = "refund-review";
  try {
    const result = await runtime.run(JSON.stringify({ orderId: "order-42" }), { identity, sessionId });
    console.log("status:", result.status, "submissions:", submissions);
    if (result.reason) console.log("reason:", result.reason.code);
    if (result.status === "completed") console.log("state:", result.structured);
    return { result, submissions, decisions };
  } finally {
    try {
      const diagnostics = await runtime.resources.closeSession(identity, sessionId);
      if (diagnostics.length) throw new Error(`Cleanup failed: ${diagnostics.join(", ")}`);
    } finally {
      approvals.close();
    }
  }
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  await reviewRefund(process.argv.includes("--approve"));
}
```

## Run both outcomes

```bash theme={null}
npx tsx harness-workflow.ts
npx tsx harness-workflow.ts --approve
```

The first run logs a denied approval and **zero submissions**. Its terminal reason is `workflow_step_failed`. The second logs an approved decision, a completed result, **one submission**, and a state containing `simulated-refund:order-42`. A denied step is visible in the result; the caller decides whether that outcome should fail a larger job.

In the downloaded project, `npm run check` verifies types and `npm start -- --approve` runs the approved path.

## Adapt it to your application

| Change | Keep this property |
| - | - |
| Replace the order fixture with a lookup | Validate the input and check eligibility in application code |
| Connect a real refund service | Authorize the resource, reuse an idempotency key, and retain the service receipt |
| Collect an operator's decision | Bind the pending approval to its verified user/tenant and selected action |
| Add another workflow step | Add the required `workflow:<step-name>` grant and test its policy/approval outcome |
| Make the process survive a restart | Use a durable task/action contract and persistent approval state; this example is process-local |

The command-line flag is a demonstration decision, not an authentication or approval UI. Move operator decisions into the host's [approval integration](/agents/approval) before exposing the workflow to users.

## Troubleshoot and clean up

* **Step denied before the callback:** check the exact `workflow:<step-name>` grant; permission and approval are separate decisions.
* **Invalid input:** the driver accepts only the declared order ID field. It does not let the caller patch `eligible` or `receipt` into trusted state.
* **Budget exhausted after adding a limit:** test the actual driver accounting. This fixed two-step example forbids model calls but leaves the aggregate tool-call budget unset; a function-step count is not a promise of identical budget consumption.
* **Unknown payment outcome after adapting it:** do not treat cancellation as a rollback. Follow [recovery](/ship/recovery) before retrying.

The program closes the session's resources and the host-owned `ApprovalManager` in `finally`. It creates no persistent data. Next, add [model-backed research](/examples/harness-research) or learn [durable approvals](/durable/overview).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.