On this page
Guides / A task that asks for human approval

A task that asks for human approval

Let a Met do the work and stop to ask permission before the step that can't be undone, with the decision coming in from your own system.

Updated View .md
StackNode · TypeScript SDK
Endpointstasks.execute · tasks.execution · tasks.approveStep · tasks.completeHumanStep

Some steps you don't want fully automated: applying a discount, sending a contract, issuing a credit note. A task can do all the preceding work and stop right before that step, waiting for a person.

This recipe wires that pause into your own system, so the decision happens where your team already works instead of in one more dashboard.

Before you start

Your key needs tasks:read and tasks:write.

The two pauses, which are not the same

who does the workhow it unblocks
Approvalthe Met, asking permission to continueapproveStep / rejectStep
Human stepthe personcompleteHumanStep

Mixing them up is the common mistake: if the Met did the work and you call completeHumanStep, the execution moves on without anyone having approved anything.

1. Fire the task

import Met from '@meteor.ia/sdk';

const met = new Met(process.env.MET_API_KEY!, {
  workspaceId: Number(process.env.MET_WORKSPACE_ID),
});

const execution = await met.tasks.execute('tsk_123');
// → { id: 'tex_…', status: 'running', … }

2. Detect that it stopped, and at which step

The execution detail carries the result of every step of that run in task_step_executions:

const state = await met.tasks.execution(execution.id);

const pending = state.task_step_executions.find(
  (s) => s.status === 'waiting_for_approval' || s.status === 'waiting_for_human',
);

if (pending) {
  await notifyYourTeam({
    execution: execution.id,
    step: pending.step_id,       // ← the step's, not its run's
    kind: pending.status,
    whatTheMetDid: pending.output,
  });
}

A step execution moves through pending, running, completed, failed, waiting_for_approval, waiting_for_human, approved and rejected.

Each row carries two identifiers: id is that run of the step, and step_id is the step inside the recipe. Approving and rejecting want the step_id. With the other one you get a 404 that reads as if the step didn't exist.

Polling is fine to start with, but in production it's better to be told: subscribe to task.completed with signed webhooks and stop asking.

3. Let your team decide from where they already work

const stepId = pending.step_id;

// Approve: the execution moves to the next step, asynchronously.
await met.tasks.approveStep(execution.id, stepId, 'Good — the numbers add up');

// Reject: that branch stops.
await met.tasks.rejectStep(execution.id, stepId, 'That discount is not authorized');

// Human step: your system did it, not the Met.
await met.tasks.completeHumanStep(execution.id, stepId, 'Contract signed and filed');

The comment you send is stored in the step's approval_comment, along with who decided and when. That's what later explains a strange run without having to reconstruct it.

All three operate on the execution id, not the task's. The step belongs to the recipe, but the pause belongs to the run: using the task id is the mistake that costs the most time here.

4. Cutting a run short

await met.tasks.cancelExecution(execution.id);

Different from pausing the task with met.tasks.setStatus(task.id, 'paused'): that stops new executions from being born and does not stop the ones already running.

What to decide before this goes to production

A stopped execution stays stopped. If nobody approves, there it sits — no timeout resolves it for you. Decide up front what happens to an approval nobody looked at in 48 hours: remind, escalate, or cancel and redo. It's a product decision, and the answer "someone will get to it" always ends in the same call from a customer asking about something left half-done.

For the same reason, a task with human pauses doesn't belong in CI: the job waits until the timeout. For unattended work, see run a Met from GitHub Actions.

Next