Lelemon
  • Get started
  • Use cases
  • Guides
  • API basics
  • Webhooks
  • SDK, CLI and MCP
  • API reference
  • Changelog
  • v1 (legacy)
Configure your account
    AgentsPipelinesKnowledgeContactsActions and MCP in v2Approve agent actionsUse Claude as your team's assistantConnect your back officeAPI keys
Talk to people
Run and protect
powered by Zudoku
Configure your account

Approve agent actions and answer the team

An agent proposes, a person decides. When a tool has an approval policy, the agent's call does not run: it becomes an approval that waits. Only an approval runs the action, and then the agent resumes the conversation with the result. The model cannot skip this step, even if the person it talks to tries to trick it: the check runs in our code, after the model and before the action.

Put an action behind a person

Grant the action to the agent with an approval policy.

TerminalCode
curl -X PUT "https://api.lelemon.cl/v2/agents/agt_5RvP2aKq9XwLmN3tYbZc01/tools/act_9Hs3x8bVw1TqS9mYp3RkLd" \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "approval": { "mode": "when", "condition": { "param": "amount", "op": "gt", "value": 20000 }, "accumulated": { "param": "amount", "period": "month", "limit": 50000 }, "approvers": [ { "type": "role", "value": "admin" }, { "type": "key", "value": "key_4Zq0x8bVw1TqS9mYp3RkLd" } ], "expiresInMinutes": 240, "escalateAfterMinutes": 60 } }'
FieldMeaning
modenever runs without approval, always waits every time, when waits when the condition holds or the accumulated limit would pass, rules lets an ordered list of rules decide.
conditionA comparison {param, op, value} on the arguments of the action, or a group {all: [...]} or {any: [...]}. op is eq, ne, gt, gte, lt, lte, in or exists. If the argument is missing or has another type, the call waits.
accumulatedThe most that may run without approval for the same contact in a rolling window of day, week or month. It stops a split into many small orders: each order is small, the sum is not.
rulesOnly for mode: rules. The ordered list that decides each call. See steps and hard caps.
approversWho may decide: a role, a team, a member, an API key or the requester. Without it, owners and admins. A role includes the roles above it: see roles.
expiresInMinutesPast it, the request expires and the action never runs.
escalateAfterMinutesFires approval.escalated once, so you can remind someone before it expires.
automatedDecisionRecords every call as an automated decision about a person, with any mode. See automated decisions.

Granting again without approval keeps the policy. To lift it, send {"approval": {"mode": "never"}}.

Who counts as an admin

An approver of type role names the lowest role that may decide, and every role above it may decide too. The order is owner, admin, member, viewer. A request assigned to role: admin is seen and decided by admins and owners. One assigned to role: owner is seen only by owners. Naming a member, a team or a key is exact: only that one decides.

Steps and hard caps with rules

mode: rules takes an ordered list. For each call the first rule whose when applies decides, and the rest are not read. Each rule has an effect:

EffectWhat happens
allowThe agent runs the action and nobody is asked.
require_approvalA person decides first. The rule may set its own approvers, expiresInMinutes, escalateAfterMinutes and customerMessage; without them the ones of the policy apply.
rejectThe action never runs and nobody is asked. The agent tells the person it cannot be done, with the rule's customerMessage if it has one. Use it for a hard cap.
Code
{ "approval": { "mode": "rules", "rules": [ { "when": { "param": "amount", "op": "gt", "value": 500000 }, "effect": "reject", "customerMessage": "A refund this large is not something we can do." }, { "when": { "param": "amount", "op": "gt", "value": 100000 }, "effect": "require_approval", "approvers": [{ "type": "role", "value": "owner" }] }, { "when": { "param": "amount", "op": "gt", "value": 20000 }, "effect": "require_approval", "approvers": [{ "type": "role", "value": "admin" }] } ] } }
  • Order the rules from the strictest to the loosest. Exactly 500,000 does not match gt 500000, so it goes to the management; 500,001 is rejected. Use gte or lte when the limit itself belongs to the rule.
  • A call that no rule matches runs. End the list with a rule that has no when to choose the default. Only the last rule may omit when: a rule after it could never run, and the request is refused.
  • If the argument a rule reads is missing, is not a number or is not finite, the call is not rejected and does not run: a person decides.
  • A platform floor and accumulated still apply on top. No rule can run an action that the floor sends to a person, and a reject always wins over a floor that would only ask. A floor itself takes never, always or when, not rules.
  • Policies saved before rules existed keep working as they were. GET shows them with the same mode.

Decide by API

List what waits, read the exact arguments, then decide with a reason.

TerminalCode
curl "https://api.lelemon.cl/v2/approvals?status=pending" \ -H "Authorization: Bearer $LELEMON_API_KEY" curl -X POST "https://api.lelemon.cl/v2/approvals/apr_5RvP2aKq9XwLmN3tYbZc01/approve" \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "reason": "Order verified in the ERP", "actor": { "id": "jira:ana", "role": "staff", "name": "Ana" } }'
Code
const { data } = await lelemon.approvals.list({ query: { status: 'pending' } }); await lelemon.approvals.approve(data[0].id, { reason: 'Order verified in the ERP' });

Approving runs the action for real. Add ?dryRun=true to approve to run the same checks (a reason, the key may decide, still pending) without deciding or running anything: the answer is the approval with status: "preview" and the decision it would record. Nothing is saved. A few rules keep it safe:

  • The key needs the approvals:write scope and the policy must name it as an approver. A key that the policy does not name gets 403 approval_decision_not_allowed.
  • The key whose turn proposed the action can never decide it.
  • A request is decided once. The second attempt gets 409 approval_not_pending, also when two decisions arrive at the same time.
  • A reason is required to approve and to reject.
  • actor is the person in your system who decided. Lelemon stores it as declared by the key, not verified.

The Lelemon dashboard has the same queue under Approvals, filtered by the approvers of each policy. Your team approves or rejects there with the same rules, and the policy of each tool is edited in the Tools tab of the agent.

A platform and its subaccounts

A platform key can be named as approver in the policy of a subaccount, and it decides acting on that subaccount with the Lelemon-Subaccount header. A key of one subaccount never decides in a sibling, and a key of another platform never decides here.

Set a floor on an action you share with your subaccounts, so none of them can run it without approval:

TerminalCode
curl -X PUT "https://api.lelemon.cl/v2/actions/act_9Hs3x8bVw1TqS9mYp3RkLd/approval-floor" \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "mode": "always", "approvers": [{ "type": "key", "value": "key_4Zq0x8bVw1TqS9mYp3RkLd" }] }'

The floor decides who approves when it applies, whatever the policy of the subaccount says, and a subaccount cannot grant the action with mode: never. The policy of the subaccount still applies on top and the stricter one wins. GET /v2/actions/{id}/approval-floor reads it, and mode: never removes it. Only a platform sets it, and only on a shared action.

What the agent is told

When a call needs a person, the agent gets a result that says the action did not run, with one of three states: pending_approval (waiting for a person), approved_running (the same request was already approved and is running; there is no result yet) or rejected_by_policy (a rule refused it; nobody is reviewing it). You see the state in the output of the step in GET /v2/cases/{id}/turns. A call that runs returns the result of the action as usual. So the agent never tells the person that something is under review when it already ran, nor announces a result that is still waiting. When the decision arrives, the agent resumes the conversation with the real outcome.

When the person asks to be erased

Erasing a contact (DELETE /v2/contacts/{id}) cancels what waits for a person about them. Approvals still pending or approved become canceled and lose their arguments, and the team questions still open are discarded. Nothing runs and the person is not notified. Each one fires approval.canceled. An approval that is already executing is left to finish.

Events

approval.requested, approval.decided, approval.expired, approval.escalated, approval.canceled, approval.review_requested and automated_decision.review_requested. Each carries the whole approval or automatedDecision.

Automated decisions

Set automatedDecision: true when the action makes a decision about a person that a machine would otherwise take (Ley 21.719, art. 8 bis). It works with any mode, so you can let small decisions run and send the risky ones to a person.

Art. 8 bis gives the person the right to information, to an explanation, to human intervention, to give their point of view and to ask for a review. The record covers each one:

The person's rightWhere it is
Information and explanationreasonCode (policy_always, policy_condition, policy_accumulated, policy_rule with its ruleIndex, or policy_not_required), and dataSeen: the names of the argument fields the decision used, never their values.
Human interventionA call the policy sends to a person is an approval a person decides. For the others, the review below.
Their point of view and a reviewrequest-review stores what they said in reviewReason and fires the webhook.

Every call to the action leaves a record. A call a person decided is an approval. A call that ran with nobody asked, or that a rule rejected, is an automated decision: GET /v2/automated-decisions lists them, filtered by caseId, agentId or outcome, and GET /v2/automated-decisions/{id} reads one. A noPersist action leaves an empty dataSeen. Test chats leave no record.

When the person asks for a review, call POST /v2/approvals/{id}/request-review for an approval that was decided, or POST /v2/automated-decisions/{id}/request-review for the other kind. Send reason with what they said. It is stored, fires approval.review_requested or automated_decision.review_requested once, and changes nothing else; asking twice keeps the first reason. It answers 422 review_not_available for an approval that nobody decided or whose policy does not mark automatedDecision.

If the person asks for a human while talking to the agent, grant transfer_to_human too. The agent should hand the case over with it instead of promising a review: request-review records the request and notifies your team, but it does not put a person in the conversation.

Let the agent ask your team

Grant the ask_team system tool. When the agent does not know something, it leaves a question and keeps attending the person. It does not hand over the case.

TerminalCode
curl -X PUT "https://api.lelemon.cl/v2/agents/agt_5RvP2aKq9XwLmN3tYbZc01/tools/ask_team" \ -H "Authorization: Bearer $LELEMON_API_KEY"

Your team answers from its own tool, for example Jira, listening to team_question.created:

TerminalCode
curl -X POST "https://api.lelemon.cl/v2/team-questions/tqn_5RvP2aKq9XwLmN3tYbZc01/answer" \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "answer": "It ships on Monday. The tracking number goes by email." }'

The agent resumes and tells the person with its own words, through the channel of the case. A question is answered once (409 team_question_not_open). If the delivery fails, a sweeper retries it for 24 hours.

To read a question with the conversation behind it and the data of the case, with every sensitive value covered, call GET /v2/team-questions/{id}/context. Use Claude as your team's assistant puts it to work.

The MCP server for the API does not offer approvals by default: LELEMON_MCP_ALLOW=approvals turns them on.

Actions and MCP in v2Use Claude as your team's assistant
On this page
  • Put an action behind a person
  • Who counts as an admin
  • Steps and hard caps with rules
  • Decide by API
    • A platform and its subaccounts
    • What the agent is told
    • When the person asks to be erased
    • Events
  • Automated decisions
  • Let the agent ask your team
JSON
TypeScript