Automate Manual Approval Processes icon Automate Manual Approval Processes

Modern automation requires human oversight for critical decisions. Kestra enables integration of manual approval steps within workflows while maintaining audit trails and process consistency.

Add human approval steps to workflows

What is Human-in-the-Loop Automation?

Human-in-the-loop (HITL) automation combines automated tasks with human decision points. Kestra implements this through:

  • Pause/Resume – Pause workflows for manual inspection before resuming
  • Dynamic Inputs – Collect user decisions during execution
  • Approval Chains – Route decisions to specific users or teams
  • Audit Logs – Track who approved/rejected each request and why.

Why Use Kestra for Human-in-the-Loop Workflows?

  1. Flexible Integration – Add approval steps to existing workflows in a few lines of YAML
  2. Enterprise Security – Manage permissions via namespace-level RBAC
  3. Cross-Platform Notifications – Send approval requests to Slack, Teams, or Email
  4. Input Validation – Enforce structured responses (Numeric, Boolean, Dates, Dropdowns)
  5. Bulk Actions – Bulk-resume multiple paused workflows when needed.
  6. Audit Trails – Track approvals, rejections, and reasons for each decision.

Example: Vacation Approval Workflow

This workflow demonstrates a complete approval process with Slack notifications and audit logging:

id: vacation_approval
namespace: hr.operations
inputs:
- id: employee
type: STRING
required: false
- id: start_date
type: DATE
- id: end_date
type: DATE
tasks:
- id: notify_manager
type: io.kestra.plugin.slack.SlackIncomingWebhook
url: "{{ secret('SLACK_HR_WEBHOOK') }}"
payload: |
{
"channel": "#vacation-approvals",
"text": "Review request from {{ inputs.employee ?? labels.system.username }}\n*Dates*: {{ inputs.start_date }} → {{ inputs.end_date }}\nApprove: {{ appLink('appId') }}"
}
- id: await_decision
type: io.kestra.plugin.core.flow.Pause
onResume:
- id: approved
type: BOOLEAN
description: Approve this request?
- id: reason
type: STRING
description: Decision notes
- id: update_hr_system
type: io.kestra.plugin.core.http.Request
uri: "{{ kv('HR_API_ENDPOINT') }}/approvals"
method: POST
contentType: multipart/form-data
formData:
employee: "{{ inputs.employee ?? labels.system.username }}"
approvalStatus: "{{ outputs.await_decision.onResume.approved ? 'APPROVED' : 'REJECTED' }}"
notes: "{{ outputs.await_decision.onResume.reason }}"
resumedBy: "{{ outputs.await_decision.resumed.by }}"
resumedOn: "{{ outputs.await_decision.resumed.on }}"
resumedStatus: "{{ outputs.await_decision.resumed.to }}" # by default: SUCCESS
- id: log_result
type: io.kestra.plugin.core.log.Log
message: |
Decision: {{ outputs.await_decision.onResume }}

Kestra Features for Human-in-the-Loop Automation

Structured Inputs for Human Decisions

Add approval steps with structured inputs to any workflow:

- id: await_decision
type: io.kestra.plugin.core.flow.Pause
onResume:
- id: approved
type: BOOLEAN
displayName: Approve this request?
- id: reason
type: STRING
displayName: Decision notes
- id: team
type: SELECT
displayName: Team to review
values:
- HR
- Finance
- IT

Bulk Actions

Approve multiple paused workflows simultaneously: Bulk Resume

Audit Trails

Audit Logs capture who approved or rejected each request, and the Pause task’s outputs contain the user’s decision:

{
"approved": true,
"reason": "Within policy limits"
}

Conditional Branching

Route next automated tasks based on human decisions:

- id: handle_rejection
type: io.kestra.plugin.core.flow.If
condition: "{{ outputs.await_decision.onResume.approved is false }}"
then:
- id: notify_employee
type: io.kestra.plugin.mail.MailSend
to: "{{ inputs.employee_email }}"
subject: "Request Denied"
htmlTextContent: "Reason: {{ outputs.await_decision.onResume.reason }}"

HumanTask: Assign Specific Users for Approval

Available on:

v>=1.1Enterprise Edition

For enterprise use cases where specific users or groups must handle approvals, use the HumanTask instead of the basic Pause task. This ensures only authorized users can resume paused executions.

Key Benefits of HumanTask

  • User-Specific Assignments – Assign approval tasks to specific users by email
  • Group-Based Permissions – Route approvals to entire RBAC groups
  • Access Control – Prevent unauthorized users from resuming executions
  • Auditability – Track who approved what for audit and compliance.

When an unauthorized user attempts to resume a HumanTask, they receive an “Access denied to resume this execution” error.

Basic HumanTask Example

id: vm_provisioning_approval
namespace: infrastructure
inputs:
- id: vm_spec
type: STRING
defaults: "2 vCPU, 4GB RAM, Ubuntu 22.04"
tasks:
- id: validate_request
type: io.kestra.plugin.core.log.Log
message: "Validating VM request from {{ labels.system.username }}: {{ inputs.vm_spec }}"
- id: it_admin_approval
type: io.kestra.plugin.ee.flow.HumanTask
assignment:
users:
- it-admin@company.com
- infrastructure-lead@company.com
- id: provision_vm
type: io.kestra.plugin.core.log.Log
message: "Approved by {{ outputs.it_admin_approval.resumed.by }}! Provisioning VM for {{ labels.system.username }}"

Group-Based Assignment

You can assign approvals to entire RBAC groups for team-based Human-in-the-loop workflows:

- id: security_review
type: io.kestra.plugin.ee.flow.HumanTask
assignment:
groups:
- Security Team
- DevOps Engineers
- Infrastructure Admins

Combined User and Group Assignment

When needed, you can also mix users and groups to allow both individual users and users from specific RBAC groups to approve the workflow:

- id: production_deployment_approval
type: io.kestra.plugin.ee.flow.HumanTask
assignment:
users:
- platform-lead@company.com
- release-manager@company.com
groups:
- DevOps Team
- Site Reliability Engineers

Best practices for long-running workflows

Long approvals can take days or weeks. Kestra persists execution state (including PAUSED state) in the database, so a paused execution survives server restarts and stays PAUSED until you manually resume it via the UI or API.

Keep downstream logic in the same flow (simplest and most common pattern)

The simplest pattern is to keep the entire downstream logic in the same flow after the Pause task:

id: pause_demo
namespace: demo
tasks:
- id: initial_logic
type: io.kestra.plugin.core.log.Log
message: placeholder for tasks with initial logic before the pause
- id: wait_for_manual_resume
type: io.kestra.plugin.core.flow.Pause
onResume:
- id: status
type: STRING
- id: entire_downstream_logic
type: io.kestra.plugin.core.log.Log
message: can have multiple tasks defined here after the pause task

Use this when it’s easier to continue in the same execution after the Pause; for larger systems, consider calling a subflow containing the downstream logic for modularity:

id: pause_demo_with_subflow
namespace: demo
tasks:
- id: initial_logic
type: io.kestra.plugin.core.log.Log
message: placeholder for tasks with initial logic before the pause
- id: wait_for_manual_resume
type: io.kestra.plugin.core.flow.Pause
onResume:
- id: status
type: STRING
- id: entire_downstream_logic
type: io.kestra.plugin.core.flow.Subflow
namespace: demo
flowId: downstream_logic_flow

Pause + Manual resume + Flow trigger pattern

  1. Flow that can stay paused as long as needed:
id: pause_demo
namespace: demo
tasks:
- id: initial_logic
type: io.kestra.plugin.core.log.Log
message: placeholder for tasks with initial logic before the pause
- id: wait_for_manual_resume
type: io.kestra.plugin.core.flow.Pause
onResume:
- id: status
type: STRING
  1. Resume the paused execution via API when ready:
curl -X POST "http://localhost:28080/api/v1/demo/executions/23F2KgSYm3uCfHJ00DvNxY/resume" \
-H 'accept: application/json' \
-F 'status=OK' -H "Authorization: Bearer your_service_account_api_token"
  1. React to the resumed flow’s completion using a Flow trigger (fires on SUCCESS of pause_demo):
id: resume_demo
namespace: demo
tasks:
- id: entire_downstream_logic
type: io.kestra.plugin.core.log.Log
message: can have multiple tasks defined here that should run after the first flow completes
triggers:
- id: flow
type: io.kestra.plugin.core.trigger.Flow
preconditions:
id: flow1
flows:
- flowId: pause_demo
namespace: demo
states:
- SUCCESS

Why this is robust:

  • The Pause task can keep the execution in PAUSED for weeks; the state is persisted in the DB and survives server restarts.
  • You resume explicitly from the UI or via API when a decision is made.
  • Downstream automation is cleanly decoupled and reacts only after the first flow completes successfully.

Understanding behavior vs. Resume/Kill from UI or API

The behavior property on a Pause task applies only when the pause duration (pauseDuration) elapses. It determines whether the workflow should automatically CANCEL or RESUME once the timer expires.

When you manually resume or kill a paused execution through the UI or API, Kestra does not apply the behavior property:

  • When you resume, the paused task ends, and the workflow continues to the next task in sequence.
  • When you kill, the entire execution transitions to the KILLING state, and all running or paused taskruns are stopped accordingly.

Example:

- id: wait_five_minutes
type: io.kestra.plugin.core.flow.Pause
behavior: CANCEL
pauseDuration: PT5M
  • If the pauseDuration elapses, the task run ends in a CANCELED state, and the execution stops.
  • If you resume manually before that time, the execution continues, ignoring the behavior property.
  • If you kill manually before that time, the execution moves to the KILLING state, ensuring all tasks are stopped.

Since Kestra 0.24, there’s no longer the need to add an explicit Kill task after the Pause task to stop the execution.


Getting Started with Human-in-the-Loop Automation

  1. Install Kestra – Follow the quick start guide or the full installation instructions for production environments.
  2. Write Your Workflows – Configure your flow in YAML. Each automated task can invoke an API, run scripts, or call any existing service. Then, add Pause tasks for manual approvals:
    - id: approval_gate
    type: io.kestra.plugin.core.flow.Pause
    onResume:
    - id: signoff
    type: BOOLEAN
    required: true
  3. Configure Notifications – Use Slack, Teams, or Email plugins to notify users about pending approvals:
    - id: alert
    type: io.kestra.plugin.teams.TeamsIncomingWebhook
    url: "{{ secret('TEAMS_WEBHOOK') }}"
    payload: |
    {
    "text": "The process {{ flow.id }} is pending approval {{ appLink() }}"
    }
  4. Add Triggers – Use scheduled or event-based triggers to launch workflows.
  5. Observe and Manage – Use Kestra’s UI to monitor states, logs, outputs, and metrics. Correct and replay failed workflow executions or roll back to a previous revision when needed.

Next Steps

Was this page helpful?