Put a person in the loop with a durable condition, and decide what happens when nobody answers.

Human gates and approvals

Use a human gate for decisions that should not be automated, such as a production deploy, a schema migration, or an agent action with real consequences. The gate is an external condition: an action that pauses a run until a signal arrives.

The wait is durable. The condition is stored on the backend, not held by a running process, so the run survives restarts, redeploys, and node failures, and can wait for days without using compute. A script that blocks on an HTTP long-poll loses the decision when its pod is rescheduled.

Choose where the person answers

Approach The person answers in Use it when
A condition The Union.ai UI The approver works in Union.ai, or needs the run’s context to decide
A Slack approval Slack, with a button The approver works in chat
A GitHub review gate The Union.ai UI, with the PR’s metadata shown The decision is about a specific pull request

A Slack approval is a condition underneath. If nobody clicks the button in Slack, the same condition can still be answered in the Union.ai UI.

Ask in Slack

slack_tasks.py
@env.task
async def deploy_with_approval(release: str, channel: str = "C0DEPLOYS") -> str:
    """Post approval buttons and wait for a click.

    `approval.request` posts Block Kit buttons and waits on a
    `flyte.new_condition`. The handler added by `approval.register` resolves
    the condition when someone clicks. The condition can also be resolved
    from the Flyte UI.
    """
    decision = await approval.request.aio(
        channel,
        f"Deploy `{release}` to prod?",
        options=["approve", "reject"],
        timeout=3600,
    )
    if decision != "approve":
        return f"{release}: not deployed ({decision})"
    return f"{release}: deployed"

The task posts Block Kit buttons and pauses the run. When someone clicks a button, the receiver signals the condition and replaces the buttons with a “decided by” line. The button’s value carries the run, action, and condition names, so the receiver needs only one setting to answer:

slack_webhooks.py
import flyte
from flyte.extras.webhooks import WebhookAppEnvironment, WebhookEvent, run_once
from flyteplugins.slack import SlackProvider, approval, events, notify

# SLACK_SIGNING_SECRET is mounted automatically. It's the signing secret from
# Basic Information, not the bot token, which goes on the task environment.
#
# `scopes` lists the channel IDs to act on: where the bot is mentioned, where
# /deploy is used, and where approvals are posted. Events from other channels
# are acknowledged but not dispatched.
app_env = WebhookAppEnvironment(
    name="slack-webhooks",
    providers=[SlackProvider()],
    scopes=["C0DEPLOYS"],
    image=flyte.Image.from_debian_base().with_pip_packages("flyteplugins-slack[app]"),
    resources=flyte.Resources(cpu=1, memory="512Mi"),
)

# Adds a handler that resolves the condition behind each button posted by
# `approval.request`. Each button's value carries the run, action, and
# condition names, so no other configuration is needed.
approval.register(app_env)

Set a timeout

A condition with no timeout waits indefinitely, and the run can sit “in progress” for weeks with nobody responsible for it.

approval.request times out after one hour by default. On expiry, wait() raises flyte.errors.ConditionTimedoutError. Catch it and decide what a timeout means:

  • No. The right default for anything with consequences. Take the safe branch.
  • Escalate. Ask again in another channel, or page someone.
  • Yes. Rarely appropriate. If an unanswered hour counts as approval, the gate does not protect anything.

Make the prompt decidable

The person answering is often not the author of the change, and may be on a phone. “Approve deploy?” sends them looking for context, so they approve without it or don’t answer.

Put the facts needed to decide in the prompt: what is changing, what it was measured against, what the measurement showed, and what happens on “no”. review_pr includes the pull request’s metadata. For Slack, build a richer Block Kit message with approval.blocks(...), including context, fields, and a link to the report. The registered handler still answers it.

Control who can answer

Anyone who can see a Slack channel can click its buttons. For a low-stakes deploy, that is acceptable. Otherwise, post to a channel whose membership is the set of approvers, or use a condition answered in the Union.ai UI, where platform permissions control access.

See Resource management for the RBAC primitives.

Keep human gates rare

A gate on every merge trains people to approve without reading. Gate on changes that are rare and consequential, such as a production promotion, a migration, or a first rollout to real traffic. If a human gate fires more than a few times a day, replace it with an evaluation gate and an alert.

Review AI output by exception

Reviewing AI output, such as a generated summary or an agent’s proposed action, differs from approving a change because its volume grows with traffic. Send only the uncertain cases to a person:

  1. Get a typed answer with a confidence score instead of free text. TypeSafe AI does this, and System one types covers the pattern.
  2. Act automatically on confident answers.
  3. Pause the uncertain ones on a condition.
  4. Record each human answer. These answers are labeled data for your next evaluation set.

See also