核心用法(3/3)
Extend agents with custom Python functions. Add API calls, file operations, or any custom logic as agent tools.
Examples:
- deterministic clicks
- file handling
- calling APIs
- human-in-the-loop
- browser interactions
- calling LLMs
- get 2fa codes
- send emails
- Playwright integration (see GitHub example)
- ...
Simply add @tools.action(...) to your function.
from browser_use import Tools, Agent, ActionResult
tools = Tools()
@tools.action(description='Ask human for help with a question')
async def ask_human(question: str) -> ActionResult:
answer = input(f'{question} > ')
return ActionResult(extracted_content=f'The human responded with: {answer}')
agent = Agent(task='...', llm=llm, tools=tools)
description(required) - What the tool does, the LLM uses this to decide when to call it.allowed_domains- List of domains where tool can run (e.g.['*.example.com']), defaults to all domains
The Agent fills your function parameters based on their names, type hints, & defaults.
Common Pitfall: Parameter names must match exactly! Use browser_session: BrowserSession (not browser: Browser).
The agent injects special parameters by name matching, so using incorrect names will cause your tool to fail silently.
See Available Objects below for the correct parameter names.
Add Tools from an MCP Server
The Python library can also import tools from an external MCP server. This extends a Browser Use agent; it is different from running Browser Use as an MCP server.
Install uv and Browser Use, and set BROWSER_USE_API_KEY for the agent's model.
Warm the reference time server's package cache before connecting:
uvx mcp-server-time==2026.8.18 --help
import asyncio
from browser_use import Agent, ChatBrowserUse, Tools
from browser_use.mcp.client import MCPClient
async def main():
tools = Tools()
client = MCPClient(
server_name="time",
command="uvx",
args=["mcp-server-time==2026.8.18", "--local-timezone=UTC"],
)
try:
await client.register_to_tools(
tools, tool_filter=["get_current_time"], prefix="time_"
)
agent = Agent(
task="Use time_get_current_time to report the current UTC time.",
llm=ChatBrowserUse(),
tools=tools,
)
await agent.run()
finally:
await client.disconnect()
asyncio.run(main())
tool_filter uses the server's original names. prefix changes the names the agent sees and avoids collisions with built-in actions, which remain available.
Keep the connection open until the agent finishes, then disconnect it. Create a fresh client for a new run.
The current client launches local stdio servers, has a roughly 10-second connection budget, and reads one tools/list response. It does not yet discover tools on later catalog pages or connect directly to HTTP/SSE endpoints.
Only run trusted servers: their subprocesses execute on your machine, and their tools may have side effects.
The time lookup itself needs no model key; running the agent above uses your configured model and may incur model charges.
Available Objects
Your function has access to these objects:
browser_session: BrowserSession- Current browser session for CDP accesscdp_client- Direct Chrome DevTools Protocol clientpage_extraction_llm: BaseChatModel- The LLM you pass into agent. This can be used to do a custom llm call here.file_system: FileSystem- File system accessavailable_file_paths: list[str]- Available files for upload/processinghas_sensitive_data: bool- Whether action contains sensitive data
Browser Interaction Examples
You can use browser_session to directly interact with page elements using CSS selectors:
from browser_use import Tools, Agent, ActionResult, BrowserSession
tools = Tools()
@tools.action(description='Click the submit button using CSS selector')
async def click_submit_button(browser_session: BrowserSession):
# Get the current page
page = await browser_session.must_get_current_page()
# Get element(s) by CSS selector
elements = await page.get_elements_by_css_selector('button[type="submit"]')
if not elements:
return ActionResult(extracted_content='No submit button found')
# Click the first matching element
await elements[0].click()
return ActionResult(extracted_content='Submit button clicked!')
Available methods on Page:
get_elements_by_css_selector(selector: str)- Returns list of matching elementsget_element_by_prompt(prompt: str, llm)- Returns element or None using LLMmust_get_element_by_prompt(prompt: str, llm)- Returns element or raises error
Available methods on Element:
click()- Click the elementtype(text: str)- Type text into the elementget_text()- Get element text content- See
browser_use/actor/element.pyfor more methods
Pydantic Input
You can use Pydantic for the tool parameters:
import json
from pydantic import BaseModel, Field
from browser_use import Tools
tools = Tools()
class Cars(BaseModel):
name: str = Field(description='The name of the car, e.g. "Toyota Camry"')
price: int = Field(description='The price of the car as int in USD, e.g. 25000')
@tools.action(description='Save cars to file')
def save_cars(cars: list[Cars]) -> str:
with open('cars.json', 'w') as f:
json.dump(cars, f)
return f'Saved {len(cars)} cars to file'
task = "find cars and save them to file"
Domain Restrictions
Limit tools to specific domains:
@tools.action(
description='Fill out banking forms',
allowed_domains=['https://mybank.com']
)
def fill_bank_form(account_number: str) -> str:
# Only works on mybank.com
return f'Filled form for account {account_number}'
Advanced Example
For a comprehensive example of custom tools with Playwright integration, see: Playwright Integration Example
This shows how to create custom actions that use Playwright's precise browser automation alongside Browser-Use.
Common Pitfalls
The agent injects special parameters by name, not by type. Using incorrect parameter names is the most common cause of tools failing silently.
❌ Wrong: Using browser: Browser
from browser_use import Tools, ActionResult, Browser
@tools.action('My action')
def my_action(browser: Browser) -> ActionResult: # WRONG!
# This will NOT receive the browser session
pass
✅ Correct: Using browser_session: BrowserSession
from browser_use import Tools, ActionResult, BrowserSession
@tools.action('My action')
async def my_action(browser_session: BrowserSession) -> ActionResult: # CORRECT!
page = await browser_session.must_get_current_page()
# Now you have access to the browser
return ActionResult(extracted_content='Done')
Key Points
- Use
browser_session: BrowserSession- notbrowser: Browser - Use
asyncfunctions - recommended for consistency with browser operations - Return
ActionResult- not plain strings (though strings work,ActionResultprovides more control) - Parameter names must match exactly - see Available Objects for the full list of injectable parameters
Attach your own CDP observer to a live V4 browser and save request metadata.
You can attach a CDP client to a live V4 browser and record its network events. This is a live observer, not a download of everything that happened during a run. V4 run events describe agent activity; they are not the browser's HTTP request log.
Find the live browser
For an agent run, watch its ordered run events for browser.ready or
browser.reattached. The event's data.browser_session_id identifies the browser.
You can also list the session's browsers with
GET /api/v4/browsers?agentSessionId=SESSION_ID. A session can use more than one
browser over time, so do not assume the first browser lasts for every follow-up.
Use an API key with browser read access in the same project to retrieve the
active browser's cdpUrl. BROWSER_SESSION_ID is a browser ID, not a run ID or
agent session ID.
export BROWSER_SESSION_ID="your-active-browser-id"
# BROWSER_USE_API_KEY must already be set. Do not print the resulting CDP URL.
set -o pipefail
BROWSER_CDP_URL="$(curl --fail --silent --show-error \
"https://api.browser-use.com/api/v4/browsers/$BROWSER_SESSION_ID" \
-H "X-Browser-Use-API-Key: $BROWSER_USE_API_KEY" \
| jq --exit-status --raw-output '.cdpUrl // empty')" || exit 1
export BROWSER_CDP_URL
For a standalone V4 browser, you can use cdpUrl from the create response
instead. A stopped browser has no live CDP URL. Check the HTTP status and browser
state if discovery fails; do not create a replacement browser and mistake it for
the agent's browser.
Treat the CDP URL as a credential. Anyone who can connect can control the browser and read its session. Keep it out of logs, tickets, source control, and shared shell history.
Record metadata from existing pages
Install Playwright with pip install playwright. No local browser download is
needed because this example connects to an existing browser.
Save this as capture_network.py, then run python capture_network.py. It records
for 30 seconds and creates network.jsonl with owner-only permissions. It refuses
to overwrite an existing file. Start it before the activity you want to observe.
import asyncio
import json
import os
from urllib.parse import urlsplit
from playwright.async_api import async_playwright
def http_origin(value):
try:
url = urlsplit(value)
if url.scheme not in {"http", "https"} or not url.hostname:
return None
host = f"[{url.hostname}]" if ":" in url.hostname else url.hostname
port = url.port
except ValueError:
return None # A malformed URL must not interrupt the capture.
default_port = {"http": 80, "https": 443}[url.scheme]
suffix = f":{port}" if port not in (None, default_port) else ""
return f"{url.scheme}://{host}{suffix}"
async def main():
async with async_playwright() as p:
cdp_url = os.environ["BROWSER_CDP_URL"]
try:
browser = await p.chromium.connect_over_cdp(cdp_url)
except Exception:
# Transport errors can include the credential-bearing CDP URL.
raise RuntimeError("Check the live CDP URL and browser state") from None
context = browser.contexts[0]
sessions = []
fd = os.open("network.jsonl", os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, "w") as output:
def write(row):
output.write(json.dumps(row) + "\n")
output.flush()
async def observe(page, page_number):
cdp = await context.new_cdp_session(page)
sessions.append(cdp)
def record(event, params):
row = {"event": event, "page": page_number,
"requestId": params["requestId"]}
if event == "request":
request = params["request"]
row.update(method=request["method"],
origin=http_origin(request["url"]),
resourceType=params.get("type"))
elif event == "response":
response = params["response"]
row.update(status=response["status"],
mimeType=response["mimeType"])
elif event == "finished":
row["bytes"] = params["encodedDataLength"]
else:
row["error"] = params.get("errorText")
write(row)
events = {
"requestWillBeSent": "request", "responseReceived": "response",
"loadingFinished": "finished", "loadingFailed": "failed",
}
for name, event in events.items():
cdp.on(f"Network.{name}",
lambda params, event=event: record(event, params))
await cdp.send("Network.enable")
try:
for number, page in enumerate(context.pages, start=1):
await observe(page, number)
print(f"Observing {len(sessions)} existing page(s) for 30 seconds")
await asyncio.sleep(30)
finally:
for cdp in sessions:
try:
await cdp.detach()
except Exception:
pass # The page or browser may already have closed.
asyncio.run(main())
Each request is correlated by (page, requestId). A response row gives the HTTP
status; a finished row gives transferred bytes. Keep the event sequence: redirects
can produce more than one request event with the same request ID. Empty output
means no matching events were observed, not that the run made no requests.
The example does not navigate, intercept requests, change the cache, or stop the browser. It drops URL paths, queries, credentials, headers, cookies, and request and response bodies. HTTP origins retain non-default ports; malformed and non-HTTP URLs have a null origin. Origins can still be sensitive; review the file before sharing it. Keep a bounded capture duration and a retention policy.
Coverage and lifecycle
- Capture starts only after
Network.enablefor each attached page. Earlier traffic is not reconstructed, and attaching afterbrowser.readycan miss startup requests. - This small example attaches only to pages present at startup. Popups, new tabs, workers, service workers, and some cross-process frames need separate target handling. It is not complete browser-wide capture.
- Reattach if V4 provisions a new browser. A logger attached to the old browser cannot observe the replacement.
- JSONL events are not HAR. A HAR exporter needs its own request, redirect,
timing, and body handling.
Network.getResponseBodyis a separate, optional CDP call; bodies may be unavailable and can contain secrets or customer data. - Detaching an observer does not stop a browser or end its billing. Leave an agent-owned browser under the agent's lifecycle. For a standalone browser you created, stop it through the browser API when finished.
See the CDP Network reference and Playwright CDP sessions for event fields and target-specific behavior.
Save a Browser Use task as a rerunnable script for repeated live data extraction and self-healing runs.
Teach a Cloud agent a repeated browser task once. It can save the working code in its workspace. Later runs reuse that script, fetch fresh data from the live site, and repair the code if the site changed.
This pattern works well for repeated data extraction, checks, and reports. The example below collects the top five Hacker News stories. The first run writes and tests the script. Every later run sends a new request to the live page.
<img alt="A first agent run saves and tests script.py and a README in a workspace; later runs reuse the script and repair it only when necessary" />
<img alt="A first agent run saves and tests script.py and a README in a workspace; later runs reuse the script and repair it only when necessary" />
The file stays. The running process does not. Each later run starts an agent again, but it can reuse the code instead of rebuilding the browser workflow.
Save the working script
Create one workspace, then ask the first run to do the job and save a tested script:
from browser_use_sdk.v4 import BrowserUse
client = BrowserUse()
workspace = client.workspaces.create(name="live-hn-data")
first = client.runs.create(
"""
Open https://news.ycombinator.com/ and return the top five story titles and
URLs as JSON.
Save a script at scripts/extract_top_stories.py that collects the same
fields. The script must send a fresh request to the live page every time it
runs. Do not hard-code story titles, save the page HTML, or read an old
result. Print JSON with source_url set to https://news.ycombinator.com/ and
include fetched_at, source_sha256, and stories.
Run the script twice and save the outputs as proof/run-1.json and
proof/run-2.json. Confirm both outputs have a fresh fetched_at value and
five stories. Add scripts/README.md with the command to run it again.
""",
model="grok-4.5",
workspace_id=workspace.id,
)
client.runs.wait_for_completion(first.id)
saved = client.workspaces.files(workspace.id, prefix="scripts/")
print([file.path for file in saved.files])
print(f"Keep this workspace ID: {workspace.id}")
import { BrowserUse } from "browser-use-sdk/v4";
const client = new BrowserUse();
const workspace = await client.workspaces.create({ name: "live-hn-data" });
const first = await client.runs.create({
task: `
Open https://news.ycombinator.com/ and return the top five story titles and
URLs as JSON.
Save a script at scripts/extract_top_stories.py that collects the same
fields. The script must send a fresh request to the live page every time it
runs. Do not hard-code story titles, save the page HTML, or read an old
result. Print JSON with source_url set to https://news.ycombinator.com/ and
include fetched_at, source_sha256, and stories.
Run the script twice and save the outputs as proof/run-1.json and
proof/run-2.json. Confirm both outputs have a fresh fetched_at value and
five stories. Add scripts/README.md with the command to run it again.
`,
model: "grok-4.5",
workspaceId: workspace.id,
});
await client.runs.waitForCompletion(first.id);
const saved = await client.workspaces.files(workspace.id, {
prefix: "scripts/",
});
console.log(saved.files.map((file) => file.path));
console.log(`Keep this workspace ID: ${workspace.id}`);
Rerun it later
Copy the workspace ID from the first process. Pass that ID without a session ID to start a new conversation with the saved files:
from browser_use_sdk.v4 import BrowserUse
client = BrowserUse()
workspace_id = "your-workspace-id"
later = client.runs.create(
"""
Run python scripts/extract_top_stories.py and return its JSON. Check that
fetched_at is current, source_url is https://news.ycombinator.com/, and it
has five stories. If the script fails or the result is malformed, inspect
the live page and repair the saved script before returning the new result.
""",
model="grok-4.5",
workspace_id=workspace_id,
)
result = client.runs.wait_for_completion(later.id)
print(result.result)
import { BrowserUse } from "browser-use-sdk/v4";
const client = new BrowserUse();
const workspaceId = "your-workspace-id";
const later = await client.runs.create({
task: `
Run python scripts/extract_top_stories.py and return its JSON. Check that
fetched_at is current, source_url is https://news.ycombinator.com/, and it
has five stories. If the script fails or the result is malformed, inspect
the live page and repair the saved script before returning the new result.
`,
model: "grok-4.5",
workspaceId,
});
const result = await client.runs.waitForCompletion(later.id);
console.log(result.result);
Live data, not a cached answer
The saved script must request the source page on every execution. fetched_at
shows when the request ran, and source_sha256 identifies the response body.
The data can stay the same between two runs when the page has not changed. The
script still fetched it again.
Do not put old output, page HTML, or fixed values inside the script. Save output files only as optional history or proof.
Can rerunnable tasks be cheaper at scale?
Reusing code can remove browser steps and model work that would otherwise be repeated on every extraction. Every later run still starts a model and uses tokens, so the savings depend on the task, site, and model. Compare the cost and duration of the first run with later runs in your own project before making a savings claim.
Workspace or session?
- Pass
workspace_id/workspaceIdfor a new conversation with the same files. The new run gets a new browser. - Pass
session_id/sessionIdto continue the old conversation and its workspace. Cloud can also reuse that session's browser while it is still alive.
A saved script can break when a site changes. Ask the agent to check the live result and self-heal the script instead of trusting stale output.
Hook into agent lifecycle events — before/after actions, navigation, extraction, and error handling callbacks.
Browser-Use provides lifecycle hooks that allow you to execute custom code at specific points during the agent's execution. Hook functions can be used to read and modify agent state while running, implement custom logic, change configuration, integrate the Agent with external applications.
Available Hooks
Currently, Browser-Use provides the following hooks:
| Hook | Description | When it is called |
|---|---|---|
on_step_start |
Executed at the beginning of each agent step | Before the agent processes the current state and decides on the next action |
on_step_end |
Executed at the end of each agent step | After the agent has executed all the actions for the current step, before it starts the next step |
await agent.run(on_step_start=..., on_step_end=...)
Each hook should be an async callable function that accepts the agent instance as its only parameter.
Basic Example
import asyncio
from pathlib import Path
from browser_use import Agent, ChatOpenAI
from browser_use.browser.events import ScreenshotEvent
async def my_step_hook(agent: Agent):
# inside a hook you can access all the state and methods under the Agent object:
# agent.settings, agent.state, agent.task
# agent.tools, agent.llm, agent.browser_session
# agent.pause(), agent.resume(), agent.add_new_task(...), etc.
# You also have direct access to the browser state
state = await agent.browser_session.get_browser_state_summary()
current_url = state.url
visit_log = agent.history.urls()
previous_url = visit_log[-2] if len(visit_log) >= 2 else None
print(f'Agent was last on URL: {previous_url} and is now on {current_url}')
cdp_session = await agent.browser_session.get_or_create_cdp_session()
# Example: Get page HTML content
doc = await cdp_session.cdp_client.send.DOM.getDocument(session_id=cdp_session.session_id)
html_result = await cdp_session.cdp_client.send.DOM.getOuterHTML(
params={'nodeId': doc['root']['nodeId']}, session_id=cdp_session.session_id
)
page_html = html_result['outerHTML']
# Example: Take a screenshot using the event system
screenshot_event = agent.browser_session.event_bus.dispatch(ScreenshotEvent(full_page=False))
await screenshot_event
result = await screenshot_event.event_result(raise_if_any=True, raise_if_none=True)
# Example: pause agent execution and resume it based on some custom code
if '/finished' in current_url:
agent.pause()
Path('result.txt').write_text(page_html)
input('Saved "finished" page content to result.txt, press [Enter] to resume...')
agent.resume()
async def main():
agent = Agent(
task='Search for the latest news about AI',
llm=ChatOpenAI(model='gpt-5-mini'),
)
await agent.run(
on_step_start=my_step_hook,
# on_step_end=...
max_steps=10,
)
if __name__ == '__main__':
asyncio.run(main())
Data Available in Hooks
When working with agent hooks, you have access to the entire Agent instance. Here are some useful data points you can access:
agent.taskprovides the main task,agent.add_new_task(...)allows you to queue up a new oneagent.toolsgives access to theTools()object andRegistry()containing the available actionsagent.tools.registry.execute_action('click', {'index': 123}, browser_session=agent.browser_session)agent.sensitive_datacontains the sensitive data dict, which can be updated in-place to add/remove/modify itemsagent.settingscontains all the configuration options passed to theAgent(...)at init timeagent.llmprovides direct access to the main LLM object (e.g.ChatOpenAI)agent.stateprovides access to internal state, including agent thoughts, outputs, actions, and moreagent.historygives access to historical data from the agent's execution:agent.history.model_thoughts(): Reasoning from Browser Use's model.agent.history.model_outputs(): Raw outputs from the Browser Use's model.agent.history.model_actions(): Actions taken by the agentagent.history.extracted_content(): Content extracted from web pagesagent.history.urls(): URLs visited by the agentagent.browser_sessionprovides direct access to theBrowserSessionand CDP interfaceagent.browser_session.agent_focus_target_id: Get the current target ID the agent is focused onagent.browser_session.get_or_create_cdp_session(): Get the current CDP session for browser interactionagent.browser_session.get_tabs(): Get all tabs currently openagent.browser_session.get_current_page_url(): Get the URL of the current active tabagent.browser_session.get_current_page_title(): Get the title of the current active tab
Tips for Using Hooks
- Avoid blocking operations: Since hooks run in the same execution thread as the agent, keep them efficient and avoid blocking operations.
- Use custom tools instead: Hooks are fairly advanced. Most use cases can be implemented with custom tools instead.
- Increase step_timeout: If your hook is doing something that takes a long time, you can increase the
step_timeoutparameter in theAgent(...)constructor.
V2 agent models and file handling.
Models
| Model | API String | Cost per Step |
|---|---|---|
| Browser Use 2.0 (default) | browser-use-2.0 |
\$0.006 |
| O3 | o3 |
\$0.03 |
| Gemini Flash Latest | gemini-flash-latest |
\$0.0075 |
| Gemini Flash Lite Latest | gemini-flash-lite-latest |
\$0.005 |
| Claude Sonnet 4.5 | claude-sonnet-4-5-20250929 |
\$0.05 |
| Claude Sonnet 4.6 | claude-sonnet-4.6 |
\$0.05 |
Pass llm explicitly to select a model:
result = await client.run("...", llm="browser-use-2.0")
const result = await client.run("...", { llm: "browser-use-2.0" });
Files
Upload images, PDFs, documents, and text files (10 MB max) to sessions, and download output files from completed tasks.
Upload a file
Get a presigned URL, then upload via POST.
import httpx
from browser_use_sdk import AsyncBrowserUse
client = AsyncBrowserUse()
session = await client.sessions.create()
upload = await client.files.session_url(
session.id,
file_name="input.pdf",
content_type="application/pdf",
size_bytes=1024,
)
with open("input.pdf", "rb") as f:
async with httpx.AsyncClient() as http:
await http.post(upload.url, content=f.read(), headers={"Content-Type": "application/pdf"})
result = await client.run("Summarize the uploaded PDF", session_id=session.id)
import { BrowserUse } from "browser-use-sdk";
import { readFileSync } from "fs";
const client = new BrowserUse();
const session = await client.sessions.create();
const upload = await client.files.sessionUrl(session.id, {
fileName: "input.pdf",
contentType: "application/pdf",
sizeBytes: 1024,
});
await fetch(upload.url, {
method: "POST",
body: readFileSync("input.pdf"),
headers: { "Content-Type": "application/pdf" },
});
const result = await client.run("Summarize the uploaded PDF", { sessionId: session.id });
Presigned URLs expire after 120 seconds. Max file size: 10 MB.
Download task output files
result = await client.tasks.get(task_id)
for file in result.output_files:
output = await client.files.task_output(task_id, file.id)
print(output.download_url) # download URL
const result = await client.tasks.get(taskId);
for (const file of result.outputFiles) {
const output = await client.files.taskOutput(taskId, file.id);
console.log(output.downloadUrl);
}
Streaming steps
Use async for to yield steps as the agent works.
from browser_use_sdk import AsyncBrowserUse
client = AsyncBrowserUse()
run = client.run("Find the most upvoted post on Reddit r/technology today")
async for step in run:
print(f"Step {step.number}: {step.next_goal}")
print(f" URL: {step.url}")
print(run.result.output) # final result after iteration
import { BrowserUse } from "browser-use-sdk";
const client = new BrowserUse();
const run = client.run("Find the most upvoted post on Reddit r/technology today");
for await (const step of run) {
console.log(`Step ${step.number}: ${step.nextGoal}`);
console.log(` URL: ${step.url}`);
}
console.log(run.result?.output); // final result after iteration
Key parameters
| Parameter | Type | Description |
|---|---|---|
task |
str |
What you want the agent to do. 1-50,000 characters. |
llm |
str |
Model override. Default: Browser Use 2.0. |
output_schema / schema |
Pydantic / Zod | Schema for structured output. |
session_id |
str |
Reuse an existing session. Omit for auto-session. |
start_url |
str |
Initial page URL. Saves steps — send the agent directly there. |
secrets |
dict |
Domain-specific credentials. See Authentication. |
allowed_domains |
list[str] |
Restrict agent to these domains only. |
session_settings |
SessionSettings |
Proxy, profile, browser config. See Profiles. |
flash_mode |
bool |
Faster but less careful navigation. |
thinking |
bool |
Enable legacy agent thinking behavior. This does not set model reasoning depth. |
thinkingLevel (REST) |
str |
Provider reasoning depth. The generated SDK parameter is pending; see Thinking levels. |
vision |
bool \| str |
Enable screenshots for the agent. |
highlight_elements |
bool |
Highlight interactive elements on the page. |
system_prompt_extension |
str |
Append custom instructions to the system prompt. |
judge |
bool |
Enable quality judge to verify output. |
skill_ids |
list[str] |
Skills the agent can use during the task. |
op_vault_id |
str |
1Password vault ID for auto-fill credentials and 2FA. |
metadata |
dict[str, str] |
Custom metadata attached to the task. |
