> ## Documentation Index
> Fetch the complete documentation index at: https://docs.idyllic.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Human-in-the-Loop

> Pause execution for user input and approval

# Human-in-the-Loop

AI applications frequently need human judgment at key decision points. A user might approve a deployment, choose between generated options, or provide feedback on a draft. Idyllic supports these patterns through state fields that signal pending input, allowing your system to wait efficiently until the user responds.

## The Pattern

Set state indicating you're waiting for input, then return from the action. The Durable Object hibernates with zero compute cost while the user decides. When they respond, their input arrives as a new action call:

```typescript theme={null}
export default class DocumentWriter extends AgenticSystem {
  @field draft = stream<string>('');
  @field status: 'idle' | 'drafting' | 'awaiting_approval' | 'published' = 'idle';
  @field pendingConfirm: { message: string } | null = null;

  @action()
  async createDocument(topic: string) {
    this.status = 'drafting';

    for await (const chunk of ai.stream(`Write about: ${topic}`)) {
      this.draft.append(chunk);
    }
    this.draft.complete();

    // Signal waiting for input
    this.status = 'awaiting_approval';
    this.pendingConfirm = { message: 'Publish this document?' };
    // Action completes—DO can hibernate
  }

  @action()
  async respond(approved: boolean, feedback?: string) {
    this.pendingConfirm = null;

    if (approved) {
      await this.publish();
      this.status = 'published';
    } else if (feedback) {
      this.status = 'drafting';
      this.draft.reset();
      for await (const chunk of ai.stream(`Revise based on: ${feedback}`)) {
        this.draft.append(chunk);
      }
      this.draft.complete();
      this.status = 'awaiting_approval';
      this.pendingConfirm = { message: 'Publish this revision?' };
    }
  }
}
```

## How It Works

There's no special pause primitive. When your action sets `pendingConfirm`, that broadcasts to clients. The action completes normally. The Durable Object becomes eligible for hibernation.

On the client, the component re-renders. When `pendingConfirm` has data, render the dialog. When the user responds, call `actions.respond()`, which wakes the Durable Object and continues the workflow.

The system can wait indefinitely at zero compute cost.

## Confirmation UI

Render based on `pendingConfirm` state:

```tsx theme={null}
function DocumentEditor() {
  const { draft, pendingConfirm, respond } = useSystem<DocumentWriter>();

  return (
    <div>
      <div className="draft">
        {draft.current}
        {draft.status === 'streaming' && <Cursor />}
      </div>

      {pendingConfirm && (
        <ConfirmDialog
          message={pendingConfirm.message}
          onApprove={() => respond(true)}
          onReject={(feedback) => respond(false, feedback)}
        />
      )}
    </div>
  );
}
```

## Multi-Step Approval

Track approval stages:

```typescript theme={null}
@field stage: 'drafting' | 'content_review' | 'legal_review' | 'published' = 'drafting';
@field pendingConfirm: { message: string; stage: string } | null = null;

@action()
async submitForReview() {
  this.stage = 'content_review';
  this.pendingConfirm = { message: 'Content approved?', stage: 'content' };
}

@action()
async approveStage(approved: boolean) {
  if (!approved) {
    this.stage = 'drafting';
    this.pendingConfirm = null;
    return;
  }

  if (this.stage === 'content_review') {
    this.stage = 'legal_review';
    this.pendingConfirm = { message: 'Legal approved?', stage: 'legal' };
  } else if (this.stage === 'legal_review') {
    this.stage = 'published';
    this.pendingConfirm = null;
    await this.publish();
  }
}
```

## Timeouts

Use `schedule()` for timeout behavior:

```typescript theme={null}
@field pendingConfirm: { message: string; expiresAt: number } | null = null;

@action()
async requestApproval() {
  this.pendingConfirm = {
    message: 'Approve deployment?',
    expiresAt: Date.now() + 24 * 60 * 60 * 1000,
  };
  this.schedule(24 * 60 * 60 * 1000, 'checkTimeout');
}

async checkTimeout() {
  if (this.pendingConfirm && Date.now() > this.pendingConfirm.expiresAt) {
    this.pendingConfirm = null;
    this.status = 'timed_out';
  }
}
```

***

## FAQ

### What if the user never responds?

The system waits indefinitely at zero cost. Implement timeouts if needed.

### How long can the system wait?

Indefinitely. State persists in SQLite. Close your browser, return weeks later—the pending confirmation is still there.

### Can I resume in a different browser?

Yes. State persists across sessions and devices. Same instance ID shows same state.

### What about authentication?

Verify user identity in the `respond` action. Check connection metadata or pass user info as arguments.
