--- title: Quickstart: Your First Interactive Automation | Tabstack description: Build a working interactive automation in under 5 minutes. Start a browser task, receive a mid-task form request from the agent, and supply values so the task can continue to completion. --- Interactive Mode is currently in beta. The API surface is stable, but behavior may change between releases. You’ll build a working interactive automation in under 5 minutes. By the end, you’ll have code that starts a browser task, receives a mid-task form request from the agent, and supplies values so the task can continue to completion. ## Prerequisites - A Tabstack API key ([get one at console.tabstack.ai](https://console.tabstack.ai)) - Node.js 20+ or Bun 1.0+ (TypeScript), or Python 3.9+ (Python) - The Tabstack SDK installed * [TypeScript](#tab-panel-107) * [Python](#tab-panel-108) Terminal window ``` npm install @tabstack/sdk ``` Terminal window ``` pip install tabstack ``` Set your API key as an environment variable: Terminal window ``` export TABSTACK_API_KEY="your-key-here" ``` Interactive Mode is not available through the Tabstack MCP server. Use the SDK directly. ## The task You’ll automate a newsletter signup. The agent navigates to a signup page, hits the email field, and pauses to ask you for the address. Your code responds with the value, and the agent submits the form. This is the core pattern for any interactive workflow: the agent does what it can autonomously, asks for what it needs, and continues when you respond. The samples below rely on the SDK’s typed event shapes. In TypeScript, switching on `event.event` narrows `event.data` automatically — no casts needed. In Python, `event.data` is a typed Pydantic model exposed via snake\_case attributes (`event.data.request_id`, `field.field_type`); the SDK aliases these to the underlying camelCase JSON keys. ## Full example - [TypeScript](#tab-panel-109) - [Python](#tab-panel-110) Create `interactive.ts`: ``` import Tabstack from '@tabstack/sdk' const client = new Tabstack({ apiKey: process.env.TABSTACK_API_KEY }) function resolveField(field: { ref: string; label: string }): string { const values: Record = { email: process.env.NEWSLETTER_EMAIL ?? '', name: process.env.NEWSLETTER_NAME ?? '' } return values[field.ref] ?? values[field.label.toLowerCase()] ?? '' } try { const stream = await client.agent.automate({ task: 'Sign up for the newsletter using the email address I provide.', url: 'https://example.com/newsletter', interactive: true }) for await (const event of stream) { if (event.event === 'interactive:form_data:request') { const { requestId, fields } = event.data const fieldValues = fields.map((f) => ({ ref: f.ref, value: resolveField(f) })) await client.agent.automateInput(requestId, { fields: fieldValues }) } if (event.event === 'complete') { console.log('Done:', event.data.finalAnswer) } if (event.event === 'error') { throw new Error(event.data.error.message) } } } catch (err) { if (err instanceof Tabstack.APIError) { if (err.status === 410) { console.error('Input request expired. Re-run the task and respond faster.') } else { console.error(`API error ${err.status}: ${err.message}`) } } throw err } ``` Create `interactive.py`: ``` import os from tabstack import Tabstack, APIStatusError client = Tabstack(api_key=os.environ["TABSTACK_API_KEY"]) def resolve_field(field) -> str: values = { "email": os.environ.get("NEWSLETTER_EMAIL", ""), "name": os.environ.get("NEWSLETTER_NAME", ""), } label = (field.label or "").lower() return values.get(field.ref) or values.get(label) or "" stream = client.agent.automate( task="Sign up for the newsletter using the email address I provide.", url="https://example.com/newsletter", interactive=True ) for event in stream: if event.event == "interactive:form_data:request": request_id = event.data.request_id field_values = [ {"ref": f.ref, "value": resolve_field(f)} for f in event.data.fields ] try: client.agent.automate_input(request_id, fields=field_values) except APIStatusError as e: if e.status_code == 410: # Input request expired before we responded (2-minute window). raise RuntimeError("Input request expired. Re-run the task and respond faster.") raise elif event.event == "complete": print("Done:", event.data.final_answer) elif event.event == "error": raise RuntimeError(event.data.error.message) ``` ## What just happened When you set `interactive: true`, the agent streams events back as it works. When it reaches a form it needs input for, it pauses and emits an `interactive:form_data:request` event instead of guessing or failing. That event carries a `requestId` and a list of `fields`. Each field has a `ref` (a stable identifier), a `label`, a `fieldType`, and a `required` flag. You call `automateInput` with the `requestId` and your values. The agent receives them and picks up where it left off. The stream continues until you get a `complete` event carrying the final result. The `resolveField` helper (or `resolve_field` in Python) is the key integration point: it maps field identifiers to the values your code has available. In production, this is where you look up user data, prompt a UI, or call a secrets store. The example uses environment variables to keep things runnable. One timing constraint: input requests expire after 2 minutes. If your code doesn’t call `automateInput` in time, the task fails with a `410 Gone` error. In production, handle this by restarting the task. ## Next steps - [Interactive Mode guide](/guides/interactive-mode/index.md): cancellation, expiry handling, security considerations, and the full event reference - [Orchestrator-in-the-loop](/guides/orchestrator-interactive-pattern/index.md): wire a parent agent to resolve interactive requests automatically, with no human in the loop - [API reference](/api/index.md): all parameters, streaming events, and error codes