GuidesHuman-in-the-loop approvals

Human-in-the-loop approvals

Some actions should never run unattended — a large payout, deleting production data, emailing your whole customer list. Eda’s approval flow pauses those actions until a human decides, then hands the outcome back to your code.

The two shapes

  • Inline — your code waits for the human (checkAndWait). Best for interactive agents where a short pause is fine.
  • Deferred — your code returns immediately and resumes later (getDecision/waitForApproval) from a webhook, job, or next request. Best for long-running or serverless work.

Inline approval

Add a require_approval policy

In the dashboard → Policies: agent *, action wire_transfer, condition amount >= 10000, effect require_approval.

Wait for the decision in code

import { Eda, EdaPendingTimeoutError } from "@eda-holding-inc/sdk";
const eda = new Eda({ apiKey: process.env.EDA_API_KEY! });
 
async function wire(amount: number, to: string) {
  try {
    const decision = await eda.checkAndWait(
      { agent: "treasury-agent", action: "wire_transfer", params: { amount, to } },
      { timeoutMs: 10 * 60_000, pollMs: 2_000 },
    );
    if (decision.status !== "approved") {
      return { ok: false, reason: decision.reason }; // denied
    }
    await bank.wire({ amount, to });
    return { ok: true };
  } catch (e) {
    if (e instanceof EdaPendingTimeoutError) {
      return { ok: false, reason: "Approval timed out — try again later." };
    }
    throw e;
  }
}

Approve in the dashboard

The action shows up in Approvals. A teammate approves or denies (optionally with a reason); checkAndWait resolves the moment they do.

Deferred approval

When you can’t hold a request open (serverless, a long job), persist the actionId and resume later.

// 1) kick off the check, store the id, return to the caller
const { actionId, status } = await eda.check({
  agent: "treasury-agent",
  action: "wire_transfer",
  params: { amount, to },
});
if (status === "pending") {
  await db.pendingTransfers.insert({ actionId, amount, to });
  return { queued: true };
}
 
// 2) later — a webhook, cron, or the next poll — resolve it
const decision = await eda.getDecision(actionId);
if (decision.status === "approved") {
  await bank.wire({ amount, to });
  await db.pendingTransfers.markDone(actionId);
}

Use waitForApproval(actionId, { timeoutMs }) if you’d rather block in a background worker than poll manually.

Telling the model what happened

For agentic loops, feed the decision back to the model as the tool result so it can respond gracefully:

if (decision.status === "pending") {
  return toolResult("This action needs human approval; I've requested it and will follow up.");
}
if (decision.status === "denied") {
  return toolResult(`A human declined this action: ${decision.reason}`);
}

Who can approve

Approvers are your team members in the workspace. Invite teammates and set roles in the dashboard; every approval is recorded with the approver’s identity in the audit log.