Banata

Core Concepts

Human Handoff

Let a person take over the browser when needed, then return control to the AI agent without losing the workflow state.

Some workflows should pause for a person.

Common examples:

  • login
  • approval
  • verification
  • account-specific actions the AI agent should not guess through

Banata supports this through human handoff.

What handoff means

Handoff does not create a separate workflow.

Instead:

  • the AI agent pauses
  • the person takes over the same browser
  • the same sandbox keeps the same files and context
  • control can return to the agent afterward

Requesting handoff

ts
await sandbox.requestHumanHandoff({
  reason: "approval",
  message: "Please review the page and continue manually.",
  resumePrompt:
    "After the human finishes, continue from the current page and summarize the result.",
});

Response shape:

ts
{
  ok: boolean;
  handoff: {
    requestId: string;
    status: "pending" | "accepted" | "completed" | "cancelled" | "expired";
    reason: "mfa" | "captcha" | "approval" | "login_failed" | "ambiguous_ui" | "file_download" | "custom";
    message: string;
    requestedBy: "agent" | "worker" | "sdk" | "dashboard";
    controller?: string;
    resumePrompt?: string;
    note?: string;
    requestedAt: number;
    acceptedAt?: number;
    completedAt?: number;
    expiresAt?: number;
    updatedAt: number;
  };
  preview: {
    status: string;
    controlMode?: "ai" | "human" | "shared";
    controller?: string;
    updatedAt?: number;
  } | null;
  pairedBrowser?: {
    previewUrl?: string;
    controlMode?: "ai" | "human" | "shared";
    controller?: string;
    handoffRequestedAt?: number;
    updatedAt?: number;
  } | null;
}

The important fields are:

  • handoff.reason
  • handoff.message
  • handoff.resumePrompt
  • handoff.status
  • preview.controlMode

In the normal flow:

  • before handoff: controlMode is ai
  • during handoff: controlMode is shared
  • after handoff completes: controlMode returns to ai

Accepting handoff

ts
await sandbox.acceptHumanHandoff({
  controller: "dashboard-user",
});

This returns the same core handoff object, but now:

  • handoff.status should be accepted
  • preview.controlMode should still be shared

Giving control back

ts
await sandbox.completeHumanHandoff({
  controller: "dashboard-user",
  returnControlTo: "ai",
  runResumePrompt: true,
});

Response shape:

ts
{
  ok: boolean;
  handoff: {
    requestId: string;
    status: "completed";
    reason: string;
    message: string;
    requestedBy: string;
    requestedAt: number;
    completedAt?: number;
    updatedAt: number;
  };
  preview: {
    status: string;
    controlMode?: "ai" | "human" | "shared";
  } | null;
  pairedBrowser?: {
    previewUrl?: string;
    controlMode?: "ai" | "human" | "shared";
  } | null;
  resume?: {
    ok: boolean;
    sessionId?: string;
    response?: unknown;
  };
}

If runResumePrompt is enabled and there is a stored resume prompt, the resume field tells you that the return-to-agent step was triggered.

After completion, preview.controlMode and pairedBrowser.controlMode should be ai.

When to use handoff

Use it when:

  • a step requires human judgment
  • login or verification is needed
  • a customer or operator must approve something
  • the AI agent should resume only after a manual checkpoint

Designing good handoff prompts

A good handoff-driven workflow usually has four parts:

  1. what the AI agent should do before stopping
  2. why it must stop
  3. what the person should do
  4. what should happen after control returns