Migrating from an approval SDK or a homegrown approval step
Many agents already have a human-in-the-loop step. It is often one of these:
- An approval SDK with a decorator that holds a function call until a person answers.
- A homegrown step: the agent posts a message to Slack or email, writes a row to a queue, and waits for someone to click Approve.
This page maps that step to Vulnify. The main difference: a typical approval step asks a person on every call to the wrapped function. Vulnify decides each call first. ALLOW runs it, REVIEW waits for a person, and BLOCK stops it. Which calls need a person is set by your policies and the risk engine. See Decisions.
How the concepts map
Section titled “How the concepts map”| Typical approval step | Vulnify | Notes |
|---|---|---|
| A client created with an API key | Vulnify(api_key=...) (Python) or new Vulnify({ apiKey }) (Node.js) |
Vulnify reads the key you pass. Register the agent and resource names in the app. |
| A decorator or wrapper that holds the call | vulnify.guard(fn, wait=..., **action) (Python) or vulnify.guard(action, fn, wait) (Node.js) |
You describe the action: agent, action, resource, destination, recordsAffected. With wait, the function runs only after approval. Python also has @vulnify.protect(**action), which does not wait: a REVIEW raises. |
| Every wrapped call goes to a person | A policy with decision: REVIEW |
Calls that match the policy wait for a person. Others are allowed or blocked by policy or by the risk engine. See Policies. |
| Approval in Slack or by email | Approval in the Vulnify app, with Approve and Deny in Slack, or from the review email | See Reviews and Slack. Review emails go to owners and admins when they are on for the organization. |
| Routing to teams or individuals | approverRoles on the policy: OWNER, ADMIN, MEMBER |
By role, not by named person. |
| Denial feedback passed to the model | VulnifyBlockedError with the decision’s reasons |
After a denial while waiting, Node.js adds Review denied and Python adds Review block. The agent receives the reasons, not a reviewer’s free-text reply. |
| Ask a person a free-form question | No equivalent | See What Vulnify does not do. |
| Approval from an MCP client | decide_action, then get_decision with waitMs |
On the Vulnify MCP server. A person approves in Vulnify, in Slack, or from the review email. |
Code before and after
Section titled “Code before and after”Before: an approval decorator
Section titled “Before: an approval decorator”A sketch of the usual pattern. The decorator asks a person on every call and runs the function only after a yes. ask_a_person stands for your SDK call or your own Slack or queue code.
def require_approval(fn): def wrapper(*args, **kwargs): answer = ask_a_person(f"Run {fn.__name__} with {args} {kwargs}?") # blocks until someone answers if answer.approved: return fn(*args, **kwargs) return f"Not approved: {answer.comment}" return wrapper
@require_approvaldef send_email(to: str, subject: str, body: str): """Send an email to the customer""" ...After: Vulnify
Section titled “After: Vulnify”The same tool, guarded by Vulnify. Your agent framework stays the same. On REVIEW, the call waits up to 5 minutes for a person to approve, then sends only if approved. On BLOCK, or a denial, the tool returns the reason to the model.
pip install vulnifyimport os
from vulnify import Vulnify, VulnifyBlockedError
vulnify = Vulnify(api_key=os.environ["VULNIFY_API_KEY"])
COMPANY_DOMAIN = "@yourcompany.com"
def send_email(to: str, subject: str, body: str): """Send an email to the customer""" ...
def send_email_tool(to: str, subject: str, body: str) -> str: """The tool you give the model. Vulnify decides before the email is sent.""" try: vulnify.guard( lambda: send_email(to, subject, body), wait={"timeout": 300, "poll": 2}, agent="OnboardingBot", action="SEND_EMAIL", resource="Customer Contacts", destination="INTERNAL" if to.lower().endswith(COMPANY_DOMAIN) else "EXTERNAL_EMAIL", records_affected=1, ) return f"Email sent to {to}." except VulnifyBlockedError as blocked: return f"Email not sent. {blocked}"Then add a policy so outbound email waits for a person. The format is on Policy files:
apiVersion: vulnify.io/v1kind: Policymetadata: name: review-external-customer-emailspec: description: A person approves agent email to addresses outside the company enabled: true action: ANY resource: ANY condition: action: SEND_EMAIL destination: EXTERNAL decision: REVIEW mode: ENFORCETo ask on every SEND_EMAIL call, drop destination from the condition. Every SEND_EMAIL then waits for a person.
On Node.js the same pattern is vulnify.guard(action, fn, { timeoutMs: 300_000 }). Adapters for LangChain, the OpenAI Agents SDK, the Vercel AI SDK, MCP servers, CrewAI, and LangGraph are on Framework adapters. A longer walkthrough is Stop an AI agent from exporting customer data.
What Vulnify does not do
Section titled “What Vulnify does not do”If you rely on any of these, plan for it before you migrate.
- No human-as-a-tool. An agent cannot ask a person a free-form question and get a text answer back. Vulnify only decides whether an action runs.
- No Discord or SMS approvals. Reviews are answered in the Vulnify app, in Slack, or from the review email.
- No free-text feedback to the model. A denial returns the decision’s reasons plus
Review denied(Node.js) orReview block(Python). There are no custom response or reject options. - No routing to named people. Approvers are chosen by role on the policy.
- No local CLI approval. The
vulnifyCLI validates, applies, and tests policies. It does not approve reviews. - No webhook when a review is resolved. Your code polls
GET /v1/events/{id}, or useswaitin the SDK. See Reviews. - Setup per action. Agents and resources are registered in the app, and each guarded call describes its action. A bare decorator with no arguments is not enough.
- Adapters for the listed frameworks only. The adapters cover the frameworks on Framework adapters. Anything else uses
guarddirectly.
Next steps
Section titled “Next steps”- Run one guarded action end to end in the Quickstart.
- Create an API key and a policy in the Vulnify app.

