# extract_structured_data
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/ai/functions/extract_structured_data



<Tip>This function uses AI and incurs costs.</Tip>

<Info>This function has multiple overloads</Info>

<Tabs>
  <Tab title="Extract From Page or Locator">
    Extracts structured data from web pages using AI-powered content analysis.

    This function provides intelligent data extraction from web pages using various strategies
    including HTML parsing, image analysis, and Markdown conversion. Or by using Text or Image Content.
    It supports extraction from entire pages or specific elements, with built-in caching and retry mechanisms.

    ```python theme={null}
    async def extract_structured_data(
        *,
        source: Page | Locator,
        data_schema: type[BaseModel] | dict[str, Any],
        prompt: str | None,
        strategy: Literal['IMAGE', 'MARKDOWN', 'HTML'],
        model: str,
        api_key: str | None,
        enable_dom_matching: bool | None,
        enable_cache: bool | None,
        max_retries: int | None,
    ) -> Any
    ```

    Extract data from web pages or specific elements using HTML, IMAGE, or MARKDOWN strategies with DOM matching support.

    ## Features and limitations

    **Features:**

    * **Smart Caching:** Hashes inputs and uses [KV Cache](https://docs.intunedhq.com/docs/01-learn/recipes/kv-cache) for persistent storage
    * **DOM Matching:** With `enable_dom_matching=True`, values match DOM elements for smart caching
    * **Multiple Strategies:** HTML, IMAGE, or MARKDOWN based on content type
    * **Flexible Models:** Use any up-to-date model from anthropic, openai or google based on your needs.

    **Limitations:**

    * **Model Variability:** Quality varies by model - experiment to find the best fit
    * **DOM Complexity:** Dynamic structures can affect caching and matching
    * **IMAGE Constraints:** Cannot capture truncated or off-screen content
    * **Schema Design:** Complex schemas may reduce accuracy

    ## Examples

    <CodeGroup>
      ```python Extract book details theme={null}
      from typing import TypedDict
      from playwright.async_api import Page
      from intuned_browser.ai import extract_structured_data
      class Params(TypedDict):
          pass
      async def automation(page: Page, params: Params, **_kwargs):
          await page.goto('https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html')
          # This will extract the book details from the page, using the HTML strategy with the gpt-5.4-mini model.
          # The data_schema is a JSON Schema dictionary that defines the structure of the data to extract.
          # You can also use a Pydantic BaseModel instead of a JSON Schema dictionary.
          book = await extract_structured_data(
              source=page,
              strategy="HTML",  # The HTML strategy is the default strategy and will be used if no strategy is provided.
              model="gpt-5.4-mini",
              data_schema={
                  "type": "object",
                  "properties": {
                      "name": {"type": "string"},
                      "price": {"type": "string"},
                      "description": {"type": "string"},
                      "in_stock": {"type": "string"}, # Must be string because we enabled DOM matching
                      "rating": {"type": "string"}
                  },
                  "required": ["name", "price"]
              },
              prompt="Extract book details from this page",
              enable_cache=True,  # since this is True, the method will call AI for the first time, and then whenever you call this method it will return cached results as long as the DOM is the same.
              enable_dom_matching=True,  # since this is True, the method will return the results mapped to the DOM elements, you MUST enable cache for this to work.
              max_retries=3
          )
          print(f"Found book: {book['name']} - {book['price']}")
      ```

      ```python Extract all books listings theme={null}
      from typing import TypedDict
      from playwright.async_api import Page
      from intuned_browser.ai import extract_structured_data
      class Params(TypedDict):
          pass
      async def automation(page: Page, params: Params, **_kwargs):
          await page.goto('https://books.toscrape.com/')
          # This will extract all the books listings from the page, using the HTML strategy with the claude-sonnet-4-5 model.
          # The data_schema is a JSON Schema dictionary that defines the structure of the data to extract.
          # You can also use a Pydantic BaseModel instead of a JSON Schema dictionary.
          books = await extract_structured_data(
              source=page,
              strategy="HTML",
              model="claude-sonnet-4-5",
              data_schema={
                  "type": "object",
                  "properties": {
                      "products": {
                          "type": "array",
                          "items": {
                              "type": "object",
                              "properties": {
                                  "title": {"type": "string"},
                                  "price": {"type": "string"},
                                  "availability": {"type": "string"}
                              }
                          }
                      }
                  }
              },
              prompt="Extract all book listings",
              enable_cache=False,  # In this example, we don't want to cache the extracted data, we want to extract the data every time.
          )
          for book in books['products']:
              print(f"{book['title']}: {book['price']}")
      ```
    </CodeGroup>

    ## Arguments

    <ResponseField name="source" type="Page | Locator">
      Playwright Page object to extract data from the entire page or Locator object
      to extract data from a specific element.
    </ResponseField>

    <ResponseField name="data_schema" type="type[BaseModel] | dict[str, Any]">
      Schema defining the structure of the data to extract. Can be either a Pydantic
      BaseModel class or a JSON Schema dictionary.
    </ResponseField>

    <ResponseField name="prompt" type="str">
      Optional prompt to guide the extraction process and provide more context.
      Defaults to None.
    </ResponseField>

    <ResponseField name="strategy" type="Literal['IMAGE', 'MARKDOWN', 'HTML']">
      Type of extraction strategy: - **"HTML"** (default) - Best for text-heavy
      pages with structured content - **"IMAGE"** - Best for visual content, charts,
      or complex layouts - **"MARKDOWN"** - Best for article-style content with
      semantic structure
    </ResponseField>

    <ResponseField name="enable_dom_matching" type="bool">
      Whether to enable DOM element matching during extraction. When enabled, all
      types in the schema must be strings to match with the DOM elements. Extraction
      results are mapped to their corresponding DOM elements and returned with
      matched results. These results are intelligently cached, allowing subsequent
      extractions with minor DOM changes to utilize the cached data for improved
      performance. Defaults to False.
    </ResponseField>

    <ResponseField name="enable_cache" type="bool">
      Whether to enable caching of extraction results. Defaults to True.
    </ResponseField>

    <ResponseField name="max_retries" type="int">
      Maximum number of retry attempts on failures. Failures can be validation
      errors, API errors, output errors, etc. Defaults to 3.
    </ResponseField>

    <ResponseField name="model" type="str">
      AI model to use for extraction. Accepts any model supported by Anthropic,
      OpenAI, or Google. Defaults to "claude-haiku-4-5-20251001".
    </ResponseField>

    <ResponseField name="api_key" type="str">
      Optional API key for AI extraction (if provided, will not be billed to your
      account). Defaults to None.
    </ResponseField>
  </Tab>

  <Tab title="Extract From Content">
    Extracts structured data from web pages using AI-powered content analysis.

    This function provides intelligent data extraction from web pages using various strategies
    including HTML parsing, image analysis, and Markdown conversion. Or by using Text or Image Content.
    It supports extraction from entire pages or specific elements, with built-in caching and retry mechanisms.

    ```python theme={null}
    async def extract_structured_data(
        *,
        content: list[ContentItem] | ContentItem,
        data_schema: type[BaseModel] | dict[str, Any],
        prompt: str | None,
        max_retries: int | None,
        enable_cache: bool | None,
        model: str,
        api_key: str | None,
    ) -> Any
    ```

    Extract data from text, image buffers, or image URLs without requiring a page source.

    ## Features and limitations

    **Features:**

    * **Smart Caching:** Hashes content and uses [KV Cache](https://docs.intunedhq.com/docs/01-learn/recipes/kv-cache) for persistent storage
    * **Multiple Content Items:** Combine text, images (buffer or URL) for comprehensive extraction
    * **Flexible Models:** Use any up-to-date model from anthropic, openai or google based on your needs.

    **Limitations:**

    * **Model Variability:** Quality varies by model - experiment to find the best fit
    * **Schema Design:** Complex schemas may reduce accuracy
    * **Content Quality:** Requires meaningful, contextual content for accurate extraction - sparse or ambiguous content produces poor results

    ## Examples

    <CodeGroup>
      ```python Basic Text Content Extraction theme={null}
      from typing import TypedDict
      from playwright.async_api import Page
      from intuned_browser.ai import extract_structured_data, TextContentItem
      class Params(TypedDict):
          pass
      async def automation(page: Page, params: Params, **_kwargs):
          # This will extract the person information from the text, using the gpt-5.4-mini model.
          text_content: TextContentItem = {
              "type": "text",
              "data": "John Doe, age 30, works as a Software Engineer at Tech Corp"
          }
          person = await extract_structured_data(
              content=text_content,
              model="gpt-5.4-mini",
              data_schema={
                  "type": "object",
                  "properties": {
                      "name": {"type": "string"},
                      "age": {"type": "number"},
                      "occupation": {"type": "string"},
                      "company": {"type": "string"}
                  },
                  "required": ["name"]
              },
              prompt="Extract person information from the text"
          )
          print(f"Found person: {person['name']}, {person['age']} years old")
      ```

      ```python List Extraction from Text Content theme={null}
      from typing import TypedDict
      from playwright.async_api import Page
      from intuned_browser.ai import extract_structured_data, TextContentItem
      class Params(TypedDict):
          pass
      async def automation(page: Page, params: Params, **_kwargs):
          text_content: TextContentItem = {
              "type": "text",
              "data": "iPhone 15 - $999, Samsung Galaxy - $899, Pixel 8 - $699"
          }
          products = await extract_structured_data(
              content=text_content,
              model="gpt-5.4-mini",
              data_schema={
                  "type": "object",
                  "properties": {
                      "products": {
                          "type": "array",
                          "items": {
                              "type": "object",
                              "properties": {
                                  "name": {"type": "string"},
                                  "price": {"type": "string"}
                              }
                          }
                      }
                  }
              },
              prompt="Extract all products"
          )
          for product in products['products']:
              print(f"{product['name']}: {product['price']}")
      ```
    </CodeGroup>

    ## Arguments

    <ResponseField name="content" type="list[ContentItem] | ContentItem">
      Content to extract data from - can be a single content item or array of
      [ContentItem](../type-references/ContentItem).
    </ResponseField>

    <ResponseField name="data_schema" type="type[BaseModel] | dict[str, Any]">
      Schema defining the expected structure of the extracted data. Can be either a
      Pydantic BaseModel class or a JSON Schema dictionary.
    </ResponseField>

    <ResponseField name="prompt" type="str">
      Optional prompt to guide the extraction process and provide more context.
      Defaults to None.
    </ResponseField>

    <ResponseField name="max_retries" type="int">
      Maximum number of retry attempts on failures. Failures can be validation
      errors, API errors, output errors, etc. Defaults to 3.
    </ResponseField>

    <ResponseField name="enable_cache" type="bool">
      Whether to enable caching of the extracted data. Defaults to True.
    </ResponseField>

    <ResponseField name="model" type="str">
      AI model to use for extraction. Accepts any model supported by Anthropic,
      OpenAI, or Google. Defaults to "claude-haiku-4-5-20251001".
    </ResponseField>

    <ResponseField name="api_key" type="str">
      Optional API key for AI extraction (if provided, will not be billed to your
      account). Defaults to None.
    </ResponseField>
  </Tab>
</Tabs>

## Returns: `Any`

The extracted structured data conforming to the provided schema.


# is_page_loaded
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/ai/functions/is_page_loaded



<Tip>This function uses AI and incurs costs.</Tip>

Uses AI vision to determine if a webpage has finished loading by analyzing a screenshot.
Detects loading spinners, blank content, or incomplete page states.

```python theme={null}
async def is_page_loaded(
    page: Page,
    *,
    model: str,
    timeout_s: int,
    api_key: str | None,
) -> bool
```

## Examples

<CodeGroup>
  ```python Check Page Loading theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser.ai import is_page_loaded
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      # Wait for page to finish loading
      await page.goto('https://sandbox.intuned.dev/')
      page_loaded = await is_page_loaded(page)
      if page_loaded:
          # Continue with scraping or interactions
          print("Page is loaded")
      else:
          # Wait longer or retry
          await page.wait_for_timeout(5000)
  ```

  ```python Loading Loop theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser.ai import is_page_loaded
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      # Keep checking until page loads
      await page.goto("https://example.com")
      attempts = 0
      while attempts < 10:  # We will retry up to 10 times with a 2-second delay between attempts.
          page_loaded = await is_page_loaded(
              page,
              model="claude-sonnet-4-5",
              timeout_s=5
          )
          if page_loaded:
              break  # If the page is loaded, break the loop.
          await page.wait_for_timeout(2000)  # Wait for 2 seconds before the next attempt.
          attempts += 1
  ```
</CodeGroup>

## Arguments

<ResponseField name="page" type="Page">
  The Playwright page to check
</ResponseField>

<ResponseField name="timeout_s" type="int">
  Screenshot timeout in seconds. Defaults to 10.
</ResponseField>

<ResponseField name="model" type="str">
  AI model to use for the check. Accepts any vision-capable model supported by
  Anthropic, OpenAI, or Google. Defaults to "claude-haiku-4-5-20251001".
</ResponseField>

<ResponseField name="api_key" type="str">
  Optional API key for the AI service (if provided, will not be billed to your
  account). Defaults to None.
</ResponseField>

## Returns: `bool`

True if page is loaded, False if still loading


# ContentItem
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/ai/type-references/ContentItem



A union type representing content items for AI data extraction from various content types.

This type alias defines the complete set of content types supported by the content-based
extract\_structured\_data function for extracting data from text, image buffers, or image URLs
without requiring a page source.

Type variants:

* `TextContentItem`: [TextContentItem](../type-references/TextContentItem) for text data extraction
* `ImageBufferContentItem`: [ImageBufferContentItem](../type-references/ImageBufferContentItem) for image data stored as bytes buffer
* `ImageUrlContentItem`: [ImageUrlContentItem](../type-references/ImageUrlContentItem) for image data accessible via URL

```python theme={null}
type ContentItem = TextContentItem | ImageBufferContentItem | ImageUrlContentItem
```

## Examples

<CodeGroup>
  ```python Text Content theme={null}
  from intuned_browser.ai import TextContentItem
  async def automation(page, params, **_kwargs):
      text_content: TextContentItem = {
          "type": "text",
          "data": "John Doe, age 30, works as a Software Engineer at Tech Corp"
      }
  ```

  ```python Image Buffer Content theme={null}
  from intuned_browser.ai import ImageBufferContentItem
  async def automation(page, params, **_kwargs):
      # Assuming you have image data as bytes
      with open("image.png", "rb") as f:
          image_data = f.read()

      image_content: ImageBufferContentItem = {
          "type": "image-buffer",
          "image_type": "png",
          "data": image_data
      }
  ```

  ```python Image URL Content theme={null}
  from intuned_browser.ai import ImageUrlContentItem
  async def automation(page, params, **_kwargs):
      image_content: ImageUrlContentItem = {
          "type": "image-url",
          "image_type": "jpeg",
          "data": "https://example.com/image.jpg"
      }
  ```
</CodeGroup>


# ImageBufferContentItem
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/ai/type-references/ImageBufferContentItem



Image buffer content item for content-based extraction.

```python theme={null}
class ImageBufferContentItem(dict)
```

## Properties

<ParamField type="str">
  The type of the content item, which is always "image-buffer".
</ParamField>

<ParamField type="str">
  The image format (e.g., "png", "jpeg", "gif", "webp").
</ParamField>

<ParamField type="bytes">
  The buffer containing the raw image data.
</ParamField>


# ImageUrlContentItem
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/ai/type-references/ImageUrlContentItem



Image URL content item for content-based extraction.

```python theme={null}
class ImageUrlContentItem(dict)
```

## Properties

<ParamField type="str">
  The type of the content item, which is always "image-url".
</ParamField>

<ParamField type="str">
  The image format (e.g., "png", "jpeg", "gif", "webp").
</ParamField>

<ParamField type="str">
  The URL of the image.
</ParamField>


# TextContentItem
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/ai/type-references/TextContentItem



Text content item for content-based extraction.

```python theme={null}
class TextContentItem(dict)
```

## Properties

<ParamField type="str">
  The type of the content item, which is always "text".
</ParamField>

<ParamField type="str">
  The text data to extract from.
</ParamField>


# click_until_exhausted
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/helpers/functions/click_until_exhausted



Repeatedly click a button until no new content appears or max clicks reached.

This function is useful for "Load More" buttons or paginated content where you need to
keep clicking until all content is loaded. It provides several stopping conditions:

* Button becomes invisible/disabled
* Maximum number of clicks reached
* No change detected in container content (when container\_locator is provided)

```python theme={null}
async def click_until_exhausted(
    page: Page,
    button_locator: Locator,
    heartbeat: Callable[[], None],
    *,
    container_locator: Locator | None,
    max_clicks: int,
    click_delay: float,
    no_change_threshold: int,
)
```

## Examples

<CodeGroup>
  ```python Load All Items theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import click_until_exhausted
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      await page.goto("https://sandbox.intuned.dev/load-more")
      load_more_button = page.locator("main main button")  # Select the main button in the main content area.
      # Click until button disappears or is disabled
      await click_until_exhausted(
          page=page,
          button_locator=load_more_button,
          max_clicks=20
      )
      # Will keep clicking the button until the button disappears or is disabled or the max_clicks is reached.
  ```

  ```python Track Container Changes theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import click_until_exhausted
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      await page.goto("https://sandbox.intuned.dev/load-more")
      load_more_button = page.locator("aside button")  # Select the button in the sidebar.
      container = page.locator('xpath=//*[@id="root"]/div[1]/main/slot/div/aside/div/div/slot/slot')  # Watch the sidebar container to detect changes.
      # This will count the elements under the container given before each click and after, if the count is the same, the function will stop.
      click_count = 0
      def heartbeat_callback():
          nonlocal click_count
          click_count += 1
          print(f"Clicked {click_count} times")
      await click_until_exhausted(
          page=page,
          button_locator=load_more_button,
          container_locator=container,
          heartbeat=heartbeat_callback,
          max_clicks=30,
          click_delay=0.5,
          no_change_threshold=0
      )
      # Will keep clicking the button until the button disappears or is disabled or the max_clicks is reached or no more content is loaded.
  ```
</CodeGroup>

## Arguments

<ResponseField name="page" type="Page">
  Playwright Page object
</ResponseField>

<ResponseField name="button_locator" type="Locator">
  Locator for the button to click repeatedly
</ResponseField>

<ResponseField name="heartbeat" type="Callable[[], None]">
  Optional callback invoked after each click. Defaults to lambda: None.
</ResponseField>

<ResponseField name="container_locator" type="Locator">
  Optional content container to detect changes. Defaults to None.
</ResponseField>

<ResponseField name="max_clicks" type="int">
  Maximum number of times to click the button. Defaults to 50.
</ResponseField>

<ResponseField name="click_delay" type="float">
  Delay after each click (in seconds). Defaults to 0.5.
</ResponseField>

<ResponseField name="no_change_threshold" type="int">
  Minimum change in content size to continue clicking. Defaults to 0.
</ResponseField>

## Returns: `None`

Function completes when clicking is exhausted


# download_file
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/helpers/functions/download_file



Downloads a file from a web page using various trigger methods. This function provides three flexible ways to initiate file downloads:

* **URL**: Creates a new page, navigates to the URL, waits for download, then automatically closes the page. Ideal for direct download links.
* **Locator**: Uses the current page to click the element and capture the resulting download. Perfect for download buttons or interactive elements.
* **Callback**: Executes the provided function with the page object and captures the first triggered download. Offers maximum flexibility for complex download scenarios.

```python theme={null}
async def download_file(
    page: Page,
    trigger: Trigger,
    *,
    timeout_s: int,
) -> Download
```

## Examples

<CodeGroup>
  ```python Download from direct URL theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import download_file
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      # Download from a direct URL, this will open the url and automatically download the content in it.
      download = await download_file(
          page,
          trigger="https://intuned-docs-public-images.s3.amazonaws.com/32UP83A_ENG_US.pdf"
      )
      file_name = download.suggested_filename
      return file_name
  ```

  ```python Locator Trigger theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import download_file
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      await page.goto("https://sandbox.intuned.dev/pdfs")
      download = await download_file(
          page,
          trigger=page.locator("xpath=//tbody/tr[1]//*[name()='svg']")
      )
      file_name = download.suggested_filename
      return file_name
  ```

  ```python Callback Trigger theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import download_file
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      await page.goto("https://sandbox.intuned.dev/pdfs")
      download = await download_file(
          page,
          trigger=lambda page: page.locator("xpath=//tbody/tr[1]//*[name()='svg']").click()
      )
      file_name = download.suggested_filename
      return file_name
  ```
</CodeGroup>

## Arguments

<ResponseField name="page" type="Page">
  The Playwright Page object to use for the download.
</ResponseField>

<ResponseField name="trigger" type="Trigger">
  The [Trigger](../type-references/Trigger) method to initiate the download.
</ResponseField>

<ResponseField name="timeout_s" type="int">
  Maximum time in seconds to wait for download to start. Defaults to 5.
</ResponseField>

## Returns: `Download`

The [Playwright Download object](https://playwright.dev/python/docs/api/class-download) representing the downloaded file.


# extract_markdown
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/helpers/functions/extract_markdown



Converts HTML content from a Playwright Page or Locator to semantic markdown format.

```python theme={null}
async def extract_markdown(
    source: Page | Locator,
) -> str
```

## Examples

<CodeGroup>
  ```python Extract Markdown from Locator theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import extract_markdown
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      await page.goto("https://books.toscrape.com/")
      header_locator = page.locator('h1').first  # First title on the page
      markdown = await extract_markdown(header_locator)  # Extract markdown from the first title
      print(markdown)
      return markdown
  ```

  ```python Extract Markdown from Page theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import extract_markdown
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      await page.goto("https://sandbox.intuned.dev/pdfs")
      markdown = await extract_markdown(page)
      print(markdown)
      return markdown
  ```
</CodeGroup>

## Arguments

<ResponseField name="source" type="Page | Locator">
  The source of the HTML content. When a Page is provided, extracts from the entire page. When a Locator is provided, extracts from that specific element.
</ResponseField>

## Returns: `str`

The markdown representation of the HTML content


# filter_empty_values
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/helpers/functions/filter_empty_values



Recursively filters out empty values from nested objects and arrays.

This function removes the following empty values:

* `None` values
* Empty strings (after trimming whitespace)
* Empty lists
* Empty dictionaries
* Lists and dictionaries that become empty after filtering their contents

```python theme={null}
def filter_empty_values(
    data: T,
) -> T
```

## Examples

<CodeGroup>
  ```python Basic Usage theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import filter_empty_values
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      # Filter empty values from dictionary
      result1 = filter_empty_values({"a": "", "b": "hello", "c": None})
      # Output: {"b": "hello"}
      print(result1)
      # Filter empty values from list
      result2 = filter_empty_values([1, "", None, [2, ""]])
      # Output: [1, [2]]
      print(result2)
      # Filter nested structures
      result3 = filter_empty_values({"users": [{"name": ""}, {"name": "John"}]})
      # Output: {"users": [{"name": "John"}]}
      print(result3)
      return "All data filtered successfully"
  ```
</CodeGroup>

## Arguments

<ResponseField name="data" type="T">
  The data structure to filter (dict, list, or any other type)
</ResponseField>

## Returns: `T`

Filtered data structure with empty values removed


# go_to_url
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/helpers/functions/go_to_url



<Info>This function has multiple overloads</Info>

<Tabs>
  <Tab title="Without AI Loading Detection">
    Navigates to a specified URL with enhanced reliability features including automatic retries with exponential backoff,
    intelligent timeout handling, and optional AI-powered loading verification.

    This function handles common navigation challenges by automatically retrying failed requests, detecting navigation hangs, and ensuring the page reaches a truly idle state.

    ```python theme={null}
    async def go_to_url(
        page: Page,
        url: str,
        *,
        timeout_s: int,
        retries: int,
        wait_for_load_state: str,
        throw_on_timeout: bool,
        wait_for_load_using_ai: Literal[False],
    ) -> None
    ```

    Use this overload for standard navigation without AI-powered loading detection.

    ## Examples

    <CodeGroup>
      ```python Without options theme={null}
      from typing import TypedDict
      from playwright.async_api import Page
      from intuned_browser import go_to_url
      class Params(TypedDict):
          pass
      async def automation(page: Page, params: Params, **_kwargs):
          await go_to_url(
              page,
              url='https://sandbox.intuned.dev/'
          )
          # At this point, go_to_url has waited for the page to be loaded and the network requests to be settled.
      ```

      ```python With options theme={null}
      from typing import TypedDict
      from playwright.async_api import Page
      from intuned_browser import go_to_url
      class Params(TypedDict):
          pass
      async def automation(page: Page, params: Params, **_kwargs):
          await go_to_url(
              page,
              url='https://intunedhq.com',
              wait_for_load_state="domcontentloaded",  # Faster than "load" state. The function automatically waits for the page to settle.
              throw_on_timeout=True,
              timeout_s=10,
              retries=3
          )
          # At this point, DOM content is loaded and go_to_url has waited for network requests to settle.
      ```
    </CodeGroup>

    ## Arguments

    <ResponseField name="page" type="Page">
      The Playwright Page object to navigate.
    </ResponseField>

    <ResponseField name="url" type="str">
      The URL to navigate to.
    </ResponseField>

    <ResponseField name="timeout_s" type="int">
      Maximum navigation time in seconds. Defaults to 30.
    </ResponseField>

    <ResponseField name="retries" type="int">
      Number of retry attempts with exponential backoff (factor: 2). Defaults to 3.
    </ResponseField>

    <ResponseField name="wait_for_load_state" type="Literal['load', 'domcontentloaded', 'networkidle', 'commit']">
      When to consider navigation succeeded. Defaults to "load".
    </ResponseField>

    <ResponseField name="throw_on_timeout" type="bool">
      Whether to raise an error on navigation timeout. When False, the function
      returns without throwing, allowing continued execution. Defaults to False.
    </ResponseField>

    <ResponseField name="wait_for_load_using_ai" type="bool">
      Set to False to disable AI-powered loading checks. Defaults to False.
    </ResponseField>
  </Tab>

  <Tab title="With AI Loading Detection">
    Navigates to a specified URL with enhanced reliability features including automatic retries with exponential backoff,
    intelligent timeout handling, and optional AI-powered loading verification.

    This function handles common navigation challenges by automatically retrying failed requests, detecting navigation hangs, and ensuring the page reaches a truly idle state.

    ```python theme={null}
    async def go_to_url(
        page: Page,
        url: str,
        *,
        timeout_s: int,
        retries: int,
        wait_for_load_state: str,
        throw_on_timeout: bool,
        wait_for_load_using_ai: Literal[True],
        model: str | None,
        api_key: str | None,
    ) -> None
    ```

    Use this overload when you need AI vision to verify the page is fully loaded by checking for loading spinners, blank content, or incomplete states.

    ## Examples

    <CodeGroup>
      ```python With AI Loading Detection theme={null}
      from typing import TypedDict
      from playwright.async_api import Page
      from intuned_browser import go_to_url
      class Params(TypedDict):
          pass
      async def automation(page: Page, params: Params, **_kwargs):
          await go_to_url(
              page,
              url='https://intunedhq.com',
              wait_for_load_using_ai=True,
              model="gpt-5.4-mini"
          )
          # The page is loaded and ready to use.
          # If the AI check fails, the method won't throw even if throw_on_timeout is true.
          # It only throws if the page times out reaching the default load state and throw_on_timeout is true.
      ```
    </CodeGroup>

    ## Arguments

    <ResponseField name="page" type="Page">
      The Playwright Page object to navigate.
    </ResponseField>

    <ResponseField name="url" type="str">
      The URL to navigate to.
    </ResponseField>

    <ResponseField name="timeout_s" type="int">
      Maximum navigation time in seconds. Defaults to 30.
    </ResponseField>

    <ResponseField name="retries" type="int">
      Number of retry attempts with exponential backoff (factor: 2). Defaults to 3.
    </ResponseField>

    <ResponseField name="wait_for_load_state" type="Literal['load', 'domcontentloaded', 'networkidle', 'commit']">
      When to consider navigation succeeded. Defaults to "load".
    </ResponseField>

    <ResponseField name="throw_on_timeout" type="bool">
      Whether to raise an error on navigation timeout. When False, the function
      returns without throwing, allowing continued execution. Defaults to False.
    </ResponseField>

    <ResponseField name="wait_for_load_using_ai" type="Literal[True]">
      Must be set to True to use this AI-powered overload. When true, uses AI vision
      to verify the page is fully loaded by checking for loading spinners, blank
      content, or incomplete states. Retries up to 3 times with 5-second delays.
      Check [is\_page\_loaded](../../ai/functions/is_page_loaded) for more details on
      the AI loading verification.
    </ResponseField>

    <ResponseField name="model" type="str">
      AI model to use for loading verification. Defaults to "gpt-5-mini-2025-08-07".
    </ResponseField>

    <ResponseField name="api_key" type="str">
      Optional API key for the AI check. Defaults to None.
    </ResponseField>
  </Tab>
</Tabs>

## Returns: `None`

Function completes when navigation is finished or fails after retries.


# process_date
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/helpers/functions/process_date



Parses various date string formats into datetime objects, returning only the date part with time set to midnight.
This utility function provides robust date parsing capabilities for a wide range of common formats.

```python theme={null}
def process_date(
    date_string: str,
) -> datetime | None
```

## Key features

* Returns only the date part (year, month, day)
* Time is always set to 00:00:00
* Supports multiple international formats
* Handles timezones and AM/PM formats

## Supported formats

The function handles these date format categories:

### Standard date formats

* `DD/MM/YYYY`: "22/11/2024", "13/12/2024"
* `MM/DD/YYYY`: "01/17/2025", "10/25/2024"
* Single-digit variants: "8/16/2019", "9/28/2024"

### Date-time combinations

* With 24-hour time: "22/11/2024 21:19:05"
* With AM/PM: "12/09/2024 9:00 AM"
* With dash separator: "12/19/2024 - 2:00 PM"

### Timezone support

* With timezone abbreviations: "10/23/2024 12:06 PM CST"
* With timezone offset: "01/17/2025 3:00:00 PM CT"

### Text month formats

* Short month: "5 Dec 2024", "11 Sep 2024"
* With time: "5 Dec 2024 8:00 AM PST"
* Full month: "November 14, 2024", "January 31, 2025, 5:00 pm"

## Examples

<CodeGroup>
  ```python Basic Usage theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import process_date
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      # Basic date string
      date1 = process_date("22/11/2024")
      print(date1)  # 2024-11-22 00:00:00
      # Date with time (time is ignored)
      date2 = process_date("5 Dec 2024 8:00 AM PST")
      print(date2)  # 2024-12-05 00:00:00
  ```

  ```python Invalid Date theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import process_date
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      # Invalid date returns None
      invalid_date = process_date("invalid date")
      print(invalid_date)  # will return None.
      if invalid_date is None:
          raise Exception("Invalid date")
      return "should not reach here"
  ```
</CodeGroup>

## Arguments

<ResponseField name="date_string" type="str">
  A string containing a date in various possible formats
</ResponseField>

## Returns: `datetime | None`

Returns a `datetime` object with only date components preserved (year, month, day), time always set to 00:00:00, or `None` if parsing fails


# resolve_url
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/helpers/functions/resolve_url



<Info>This function has multiple overloads</Info>

<Tabs>
  <Tab title="Base URL String">
    Converts any URL source to an absolute, properly encoded URL.

    ```python theme={null}
    async def resolve_url(
        *,
        url: str,
        base_url: str,
    ) -> str
    ```

    Combines a relative URL with a base URL string. Use when you have an explicit base URL string to resolve relative paths against.

    ## Examples

    <CodeGroup>
      ```python Resolve from Base URL String theme={null}
      from typing import TypedDict
      from playwright.async_api import Page
      from intuned_browser import resolve_url
      class Params(TypedDict):
          pass
      async def automation(page: Page, params: Params, **_kwargs):
          # Resolve from base URL string
          absolute_url = await resolve_url(
              url="/lists/table",
              base_url="https://sandbox.intuned.dev"
          )
          # Returns: "https://sandbox.intuned.dev/lists/table"
          print(absolute_url)
          return absolute_url
      ```
    </CodeGroup>

    ## Arguments

    <ResponseField name="url" type="str">
      The relative or absolute URL to resolve.
    </ResponseField>

    <ResponseField name="base_url" type="str">
      Base URL string to resolve relative URLs against.
    </ResponseField>
  </Tab>

  <Tab title="Current Page's URL">
    Converts any URL source to an absolute, properly encoded URL.

    ```python theme={null}
    async def resolve_url(
        *,
        url: str,
        page: Page,
    ) -> str
    ```

    Uses the current page's URL as the base URL. Use when resolving URLs relative to the current page.

    ## Examples

    <CodeGroup>
      ```python Resolve from the Current Page's URL theme={null}
      from typing import TypedDict
      from playwright.async_api import Page
      from intuned_browser import resolve_url
      class Params(TypedDict):
          pass
      async def automation(page: Page, params: Params, **_kwargs):
          await page.goto("https://sandbox.intuned.dev/")
          # Resolve from the current page's URL
          absolute_url = await resolve_url(
              url="/lists/table",
              page=page
          )
          # Returns: "https://sandbox.intuned.dev/lists/table"
          print(absolute_url)
          return absolute_url
      ```
    </CodeGroup>

    ## Arguments

    <ResponseField name="url" type="str">
      The relative or absolute URL to resolve.
    </ResponseField>

    <ResponseField name="page" type="Page">
      Playwright Page object to extract base URL from. The current page URL will be used as the base URL.
    </ResponseField>
  </Tab>

  <Tab title="Anchor Elements">
    Converts any URL source to an absolute, properly encoded URL.

    ```python theme={null}
    async def resolve_url(
        *,
        url: Locator,
    ) -> str
    ```

    Extracts the href attribute from a Playwright Locator pointing to an anchor element. Use when extracting and resolving URLs from anchor (`<a>`) elements.

    ## Examples

    <CodeGroup>
      ```python Resolve from Anchor Element theme={null}
      from typing import TypedDict
      from playwright.async_api import Page
      from intuned_browser import resolve_url
      class Params(TypedDict):
          pass
      async def automation(page: Page, params: Params, **_kwargs):
          await page.goto("https://sandbox.intuned.dev/")
          # Resolve from Anchor Element
          absolute_url = await resolve_url(
              url=page.locator("xpath=//a[normalize-space()='Steps Form']"),
          )
          # Returns: "https://sandbox.intuned.dev/steps-form"
          print(absolute_url)
          return absolute_url
      ```
    </CodeGroup>

    ## Arguments

    <ResponseField name="url" type="Locator">
      Playwright Locator pointing to an anchor element. The href attribute will be extracted and resolved relative to the current page.
    </ResponseField>
  </Tab>
</Tabs>

## Returns: `str`

The absolute, properly encoded URL string


# sanitize_html
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/helpers/functions/sanitize_html



Sanitizes and cleans HTML content by removing unwanted elements, attributes, and whitespace.
Provides fine-grained control over each cleaning operation through configurable options.

```python theme={null}
def sanitize_html(
    html: str,
    *,
    remove_scripts: bool,
    remove_styles: bool,
    remove_svgs: bool,
    remove_comments: bool,
    remove_long_attributes: bool,
    max_attribute_length: int,
    preserve_attributes: list[str] | None,
    remove_empty_tags: bool,
    preserve_empty_tags: list[str] | None,
    minify_whitespace: bool,
) -> str
```

## Examples

<CodeGroup>
  ```python Basic Sanitization theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import sanitize_html
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      await page.goto("https://books.toscrape.com")
      first_row = page.locator("ol.row").locator("li").first
      # Get the HTML of the first row.
      html = await first_row.inner_html()
      # Sanitize the HTML.
      sanitized_html = sanitize_html(html)
      # Log the sanitized HTML.
      print(sanitized_html)
      # Return the sanitized HTML.
      return sanitized_html
  ```
</CodeGroup>

## Arguments

<ResponseField name="html" type="str">
  The HTML content to sanitize
</ResponseField>

<ResponseField name="remove_scripts" type="bool">
  Remove all `<script>` elements. Defaults to True.
</ResponseField>

<ResponseField name="remove_styles" type="bool">
  Remove all `<style>` elements. Defaults to True.
</ResponseField>

<ResponseField name="remove_svgs" type="bool">
  Remove all `<svg>` elements. Defaults to True.
</ResponseField>

<ResponseField name="remove_comments" type="bool">
  Remove HTML comments. Defaults to True.
</ResponseField>

<ResponseField name="remove_long_attributes" type="bool">
  Remove attributes longer than max\_attribute\_length. Defaults to True.
</ResponseField>

<ResponseField name="max_attribute_length" type="int">
  Maximum length for attributes before removal. Defaults to 500.
</ResponseField>

<ResponseField name="preserve_attributes" type="list[str]">
  List of attribute names to always preserve. Defaults to \["class", "src"].
</ResponseField>

<ResponseField name="remove_empty_tags" type="bool">
  Remove empty tags (except preserved ones). Defaults to True.
</ResponseField>

<ResponseField name="preserve_empty_tags" type="list[str]">
  List of tag names to preserve even when empty. Defaults to \["img"].
</ResponseField>

<ResponseField name="minify_whitespace" type="bool">
  Remove extra whitespace between tags and empty lines. Defaults to True.
</ResponseField>

## Returns: `str`

The sanitized HTML string


# save_file_to_s3
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/helpers/functions/save_file_to_s3



Downloads a file from a web page and automatically uploads it to AWS S3 storage in a single operation.

Combines [download\_file](./download_file) (for trigger methods) and [upload\_file\_to\_s3](./upload_file_to_s3)
(for S3 configuration), providing a streamlined workflow for capturing and storing files.

```python theme={null}
async def save_file_to_s3(
    page: Page,
    trigger: Trigger,
    *,
    timeout_s: int,
    configs: S3Configs | None,
    file_name_override: str | None,
    content_type: str | None,
) -> Attachment
```

## Examples

<CodeGroup>
  ```python URL Trigger theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import save_file_to_s3, S3Configs
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      uploaded_file = await save_file_to_s3(
          page=page,
          trigger="https://intuned-docs-public-images.s3.amazonaws.com/27UP600_27UP650_ENG_US.pdf",
          configs=S3Configs(
              bucket_name='document-storage',
              region='us-east-1',
              access_key='accessKeyId',
              secret_key='SecretAccessKeyId'
          ),
          file_name_override='reports/monthly-report.pdf'
      )
      return {"file": uploaded_file}
  ```

  ```python Locator Trigger theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import save_file_to_s3
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      await page.goto("https://sandbox.intuned.dev/pdfs")
      uploaded_file = await save_file_to_s3(
          page=page,
          trigger=page.locator("xpath=//tbody/tr[1]//*[name()='svg']"),
          timeout_s=10
      )
      download_url = await uploaded_file.get_signed_url(7200)  # 2 hours
      print(f"Temporary access: {download_url}")
  ```
</CodeGroup>

## Arguments

<ResponseField name="page" type="Page">
  The Playwright Page object to use for downloading
</ResponseField>

<ResponseField name="trigger" type="Trigger">
  The [Trigger](../type-references/Trigger) method to initiate the download.
</ResponseField>

<ResponseField name="timeout_s" type="int">
  Maximum time in seconds to wait for download to start. Defaults to 5.
</ResponseField>

<ResponseField name="configs" type="S3Configs">
  Optional [S3Configs](../type-references/S3Configs) to customize the S3 upload.
  If not provided, uses environment variables (`AWS_ACCESS_KEY_ID`,
  `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`, `AWS_ENDPOINT_URL`, `AWS_BUCKET`). If
  environment variables aren't set, uses default Intuned S3 settings.
</ResponseField>

<ResponseField name="file_name_override" type="str">
  Optional custom filename for the uploaded file. If not provided, uses the
  original filename or generates a unique name.
</ResponseField>

<ResponseField name="content_type" type="str">
  Optional MIME type for the uploaded file (e.g., "application/pdf",
  "image/png"). If None, uses the original content type.
</ResponseField>

## Returns: `Attachment`

An [Attachment](../type-references/Attachment) object with file metadata and S3 utilities


# scroll_to_load_content
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/helpers/functions/scroll_to_load_content



Automatically scrolls through infinite scroll content by repeatedly scrolling to the bottom
until no new content loads or maximum scroll limit is reached.

```python theme={null}
async def scroll_to_load_content(
    source: Page | Locator,
    *,
    on_scroll_progress: Callable[[], None],
    max_scrolls: int,
    delay_s: float,
    min_height_change: int,
)
```

## Examples

<CodeGroup>
  ```python Basic Infinite Scroll handling theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import scroll_to_load_content
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      # Scroll through entire page content
      await page.goto("https://sandbox.intuned.dev/infinite-scroll")
      await scroll_to_load_content(
          source=page,
      )
      # Will keep scrolling until the page has loaded all content or the max_scrolls is reached.
  ```

  ```python Scroll Specific Container theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import scroll_to_load_content
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      # Scroll through a specific scrollable div
      await page.goto("https://docs.intunedhq.com/docs/00-getting-started/introduction")
      # This will scroll the sidebar content only and watch its height to change.
      container = page.locator("xpath=//div[@id='sidebar-content']")
      await scroll_to_load_content(
          source=container,
          max_scrolls=20
      )
      # Will keep scrolling until the sidebar content has loaded all content or the max_scrolls is reached.
  ```
</CodeGroup>

## Arguments

<ResponseField name="source" type="Page | Locator">
  The Playwright Page or Locator to scroll.
</ResponseField>

<ResponseField name="on_scroll_progress" type="Callable">
  Optional callback function to call during each scroll iteration. Defaults to lambda: None.
</ResponseField>

<ResponseField name="max_scrolls" type="int">
  Maximum number of scroll attempts before stopping. Defaults to 50.
</ResponseField>

<ResponseField name="delay_s" type="float">
  Delay in seconds between scroll attempts. Defaults to 0.1.
</ResponseField>

<ResponseField name="min_height_change" type="int">
  Minimum height change in pixels required to continue scrolling. Defaults to 100. If the page has loaded all content and we still haven't reached the max\_scrolls, the min\_height\_change will detect that no new content is loaded and stop the scrolling.
</ResponseField>

## Returns: `None`

Function completes when scrolling is finished


# upload_file_to_s3
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/helpers/functions/upload_file_to_s3



Uploads files to AWS S3 storage with flexible configuration options.

This function accepts various file types including Playwright Download objects, binary data,
making it versatile for different upload scenarios. It automatically handles file metadata
and provides comprehensive S3 configuration options.

```python theme={null}
async def upload_file_to_s3(
    file: FileType,
    *,
    configs: S3Configs | None,
    file_name_override: str | None,
    content_type: str | None,
) -> Attachment
```

## S3 configuration fallback

The function uses a fallback system to determine S3 settings:

1. **S3Configs Parameter** - If provided, uses the explicit `S3Configs` object with your custom settings.
2. **Environment Variables** - If no configs provided, automatically reads from environment variables:
   * `AWS_ACCESS_KEY_ID` - Your AWS access key
   * `AWS_SECRET_ACCESS_KEY` - Your AWS secret key
   * `AWS_REGION` - AWS region (e.g., "us-west-1")
   * `AWS_BUCKET` - S3 bucket name
   * `AWS_ENDPOINT_URL` - Optional custom S3 endpoint
   * Check [Environment Variables & Secrets](https://docs.intunedhq.com/docs/02-features/environment-variables-secrets) to learn more about setting environment variables.
3. **Intuned Defaults** - If environment variables aren't set, falls back to Intuned's managed S3 storage. See [S3 Attachment Storage](https://docs.intunedhq.com/docs/04-integrations/s3/s3-attachment-storage) for more details.

## Examples

<CodeGroup>
  ```python Upload Downloaded File theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import download_file, upload_file_to_s3
  from intuned_browser import S3Configs
  import os
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      await page.goto("https://sandbox.intuned.dev/pdfs")
      download = await download_file(
          page,
          trigger=page.locator("xpath=//tbody/tr[1]//*[name()='svg']")
      )
      # Set your environment variables for the AWS credentials.
      # Check https://docs.intunedhq.com/docs/02-features/environment-variables-secrets to learn more about setting environment variables.
      uploaded_file = await upload_file_to_s3(
          file=download,
          configs=S3Configs(
              bucket_name=os.environ['AWS_BUCKET'],
              region=os.environ['AWS_REGION'],
              access_key=os.environ['AWS_ACCESS_KEY_ID'],
              secret_key=os.environ['AWS_SECRET_ACCESS_KEY']
          ),
          file_name_override='reports/monthly-report.pdf'
      )
      print(f"File uploaded: {uploaded_file.suggested_file_name}")
  ```

  ```python Upload Binary Data theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import upload_file_to_s3
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      file_buffer = b'Hello World'
      uploaded_file = await upload_file_to_s3(
          file=file_buffer,
          file_name_override='data/text-file.txt',
          content_type='text/plain'
      )
      # Generate a temporary download URL
      download_url = await uploaded_file.get_signed_url()
      print(f"Download URL: {download_url}")
      return {
          'download_url': download_url
      }
  ```
</CodeGroup>

## Arguments

<ResponseField name="file" type="FileType">
  The file to upload. See [FileType](../type-references/FileType) for supported types.
</ResponseField>

<ResponseField name="configs" type="S3Configs">
  Optional [S3Configs](../type-references/S3Configs) for customizing the S3 upload. If not provided, uses environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`, `AWS_ENDPOINT_URL`, `AWS_BUCKET`). If environment variables aren't set, uses default Intuned S3 settings.
</ResponseField>

<ResponseField name="file_name_override" type="str">
  Optional custom filename for the uploaded file. If not provided, uses the original filename or generates a unique name.
</ResponseField>

<ResponseField name="content_type" type="str">
  Optional MIME type for the uploaded file (e.g., "application/pdf", "image/png"). If None, uses the original content type.
</ResponseField>

## Returns: `Attachment`

An [Attachment](../type-references/Attachment) object with file metadata and utility methods


# validate_data_using_schema
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/helpers/functions/validate_data_using_schema



Validates data against a JSON schema using the AJV validator.
This function can validate any given data against a given schema. It supports validating custom data types such as the [Attachment](../type-references/Attachment) type.

```python theme={null}
def validate_data_using_schema(
    data: dict[str, Any] | list[dict[str, Any]],
    schema: dict[str, Any],
)
```

## Examples

<CodeGroup>
  ```python Basic Validation theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import validate_data_using_schema
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      upload_data = {
          "file": {
              "file_name": "documents/report.pdf",
              "bucket": "my-bucket",
              "region": "us-east-1",
              "key": "documents/report.pdf",
              "endpoint": None,
              "suggested_file_name": "Monthly Report.pdf",
              "file_type": "document"
          },
          "name": "Test File Upload"
      }
      upload_schema = {
          "type": "object",
          "required": ["file", "name"],
          "properties": {
              "file": {"type": "attachment"},
              "name": {"type": "string"}
          }
      }
      validate_data_using_schema(upload_data, upload_schema)
      # Validation passes with Attachment type
      print("Validation passed")
      return "Validation passed"
  ```

  ```python Invalid Data Validation theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import validate_data_using_schema
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      user_data = {
          "name": "John Doe",
          "email": "john@example.com",
      }
      user_schema = {
          "type": "object",
          "required": ["name", "email", "age"],
          "properties": {
              "name": {"type": "string", "minLength": 1},
              "email": {"type": "string", "format": "email"},
              "age": {"type": "number", "minimum": 0}
          }
      }
      validate_data_using_schema(user_data, user_schema)  # this will throw
      # Validation fails, throws ValidationError
      print("Never reached")
      return "Never reached"
  ```
</CodeGroup>

## Arguments

<ResponseField name="data" type="dict[str, Any] | list[dict[str, Any]]">
  The data to validate. Can be a single data object or an array of data objects.
</ResponseField>

<ResponseField name="schema" type="dict[str, Any]">
  JSON schema object defining validation rules
</ResponseField>

## Returns: `None`

Returns nothing if validation passes, raises ValidationError if it fails

## Raises

### ValidationError

If validation fails


# wait_for_dom_settled
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/helpers/functions/wait_for_dom_settled



<Info>This function has multiple overloads</Info>

<Tabs>
  <Tab title="Direct Function Call (Callable Pattern)">
    Waits for DOM mutations to settle. This helper uses a MutationObserver to monitor DOM changes and waits until the DOM has been stable (no mutations) for the specified settle\_duration.

    ## `wait_for_dom_settled` vs `wait_for_network_settled`

    * Use `wait_for_dom_settled` when watching **DOM mutations** (elements added/removed/modified, loading spinners, dynamic content injection)
    * Use [wait\_for\_network\_settled](../functions/wait_for_network_settled) when watching **network requests** (API calls, form submissions, resource loading)

    ```python theme={null}
    async def wait_for_dom_settled(
        source: Page | Locator,
        *,
        settle_duration: float,
        timeout_s: float,
    ) -> bool
    ```

    This pattern waits for existing DOM changes to complete without triggering any new actions. Useful after navigation or for waiting for animations.

    ## Examples

    <CodeGroup>
      ```python Wait After Navigation theme={null}
      from typing import TypedDict
      from playwright.async_api import Page
      from intuned_browser import wait_for_dom_settled
      class Params(TypedDict):
          pass
      async def automation(page: Page, params: Params, **_kwargs):
          # Navigate to page with dynamic content
          await page.goto('https://sandbox.intuned.dev/lists/table')
          # Wait for all DOM mutations to complete
          settled = await wait_for_dom_settled(
              source=page,
              settle_duration=1.0,
              timeout_s=20
          )
          if settled:
              # DOM is stable, safe to extract data
              rows = await page.locator('table tr').all()
              return len(rows)
          return 0
      ```

      ```python Watch Specific Container theme={null}
      from typing import TypedDict
      from playwright.async_api import Page
      from intuned_browser import wait_for_dom_settled
      class Params(TypedDict):
          pass
      async def automation(page: Page, params: Params, **_kwargs):
          await page.goto("https://sandbox.intuned.dev/lists/table")
          # Only watch table container (ignore header/footer changes)
          table = page.locator("table")
          settled = await wait_for_dom_settled(
              source=table,
              settle_duration=0.8,
              timeout_s=15,
          )
          if settled:
              # Table has finished loading
              rows = await table.locator('tr').count()
              return rows
          return 0
      ```
    </CodeGroup>

    ## Arguments

    <ResponseField name="source" type="Page | Locator">
      Playwright Page or Locator object to monitor for DOM changes. Can be passed as positional or keyword argument. Use Page to monitor entire document, or Locator to watch specific element.
    </ResponseField>

    <ResponseField name="settle_duration" type="float">
      Duration in seconds that the DOM must remain stable (no mutations) to be considered "settled". Defaults to 0.5.
    </ResponseField>

    <ResponseField name="timeout_s" type="float">
      Maximum seconds to wait for DOM to settle before raising an error. Defaults to 30.0.
    </ResponseField>
  </Tab>

  <Tab title="Wrapper Function Pattern">
    Waits for DOM mutations to settle. This helper uses a MutationObserver to monitor DOM changes and waits until the DOM has been stable (no mutations) for the specified settle\_duration.

    ## `wait_for_dom_settled` vs `wait_for_network_settled`

    * Use `wait_for_dom_settled` when watching **DOM mutations** (elements added/removed/modified, loading spinners, dynamic content injection)
    * Use [wait\_for\_network\_settled](../functions/wait_for_network_settled) when watching **network requests** (API calls, form submissions, resource loading)

    ```python theme={null}
    async def wait_for_dom_settled(
        *,
        source: Page | Locator,
        func: Callable[[], Awaitable[Any]],
        settle_duration: float,
        timeout_s: float,
    ) -> Any
    ```

    This pattern executes a function and waits for DOM mutations to settle before returning.

    ## Examples

    <CodeGroup>
      ```python Wrapper with Inline Function theme={null}
      from typing import TypedDict
      from playwright.async_api import Page
      from intuned_browser import wait_for_dom_settled
      class Params(TypedDict):
          pass
      async def automation(page: Page, params: Params, **_kwargs):
          await page.goto("https://sandbox.intuned.dev/lists/table")
          # Define action inline
          async def select_card():
              await page.locator("xpath=//button[@id='radix-:r0:-trigger-card']").click()
              return "card selected"
          # Execute and wait for DOM to settle
          result = await wait_for_dom_settled(
              source=page,
              func=select_card,
              settle_duration=1.0,
              timeout_s=15
          )
          return result
      ```

      ```python Wrapper with Specific Element theme={null}
      from typing import TypedDict
      from playwright.async_api import Page
      from intuned_browser import wait_for_dom_settled
      class Params(TypedDict):
          pass
      async def automation(page: Page, params: Params, **_kwargs):
          await page.goto("https://sandbox.intuned.dev")
          # Monitor only the data table, not the entire page
          data_table = page.locator("table")
          async def click_list_extractors():
              await page.get_by_text("List Extractors").click()
              return "list extractors clicked"
          await wait_for_dom_settled(
              source=data_table,  # Only watch this element
              func=click_list_extractors,
              settle_duration=0.8,
              timeout_s=10
          )
          # Table has finished updating
          rows = await data_table.locator("tr").count()
          return rows
      ```

      ```python Wrapper with Lambda theme={null}
      from typing import TypedDict
      from playwright.async_api import Page
      from intuned_browser import wait_for_dom_settled
      class Params(TypedDict):
          pass
      async def automation(page: Page, params: Params, **_kwargs):
          await page.goto("https://sandbox.intuned.dev/load-more")
          # Quick one-off action
          await wait_for_dom_settled(
              source=page,
              func=lambda: page.locator("main main button").click(),
              settle_duration=0.5,
              timeout_s=10
          )
          # More items loaded
          items = await page.locator(".item").count()
          return items
      ```
    </CodeGroup>

    ## Arguments

    <ResponseField name="source" type="Page | Locator">
      Playwright Page or Locator object to monitor for DOM changes. Use Page for entire document, Locator for specific element.
    </ResponseField>

    <ResponseField name="func" type="Callable[[], Any]">
      The async function to execute before waiting for DOM to settle. This function should contain the action that triggers DOM changes.
    </ResponseField>

    <ResponseField name="settle_duration" type="float">
      Duration in seconds that the DOM must remain stable (no mutations) to be considered "settled". Defaults to 0.5.
    </ResponseField>

    <ResponseField name="timeout_s" type="float">
      Maximum seconds to wait for DOM to settle before raising an error. Defaults to 30.0.
    </ResponseField>
  </Tab>

  <Tab title="Decorator">
    Waits for DOM mutations to settle. This helper uses a MutationObserver to monitor DOM changes and waits until the DOM has been stable (no mutations) for the specified settle\_duration.

    ## `wait_for_dom_settled` vs `wait_for_network_settled`

    * Use `wait_for_dom_settled` when watching **DOM mutations** (elements added/removed/modified, loading spinners, dynamic content injection)
    * Use [wait\_for\_network\_settled](../functions/wait_for_network_settled) when watching **network requests** (API calls, form submissions, resource loading)

    ```python theme={null}
    def wait_for_dom_settled(
        func: Callable[..., Awaitable[Any]],
    ) -> Callable[..., Awaitable[Any]]
    ```

    This pattern decorates a function to automatically wait for DOM to settle after execution.

    ## Examples

    <CodeGroup>
      ```python Simple Decorator theme={null}
      from typing import TypedDict
      from playwright.async_api import Page
      from intuned_browser import wait_for_dom_settled
      class Params(TypedDict):
          pass
      async def automation(page: Page, params: Params, **_kwargs):
          await page.goto("https://sandbox.intuned.dev/load-more")
          # Decorator without arguments (uses settle_duration=0.5, timeout_s=30.0)
          @wait_for_dom_settled
          async def load_more_content(page):
              await page.locator("main main button").click()
          # Automatically waits for DOM to settle after clicking
          await load_more_content(page)
          # DOM has settled, new content is loaded
      ```
    </CodeGroup>

    ## Arguments

    <ResponseField name="func" type="Callable[[Page], Any] | Callable[[Locator], Any]">
      The async function to decorate. Must accept a Page or Locator object as a parameter. If not provided, returns a parameterized decorator.
    </ResponseField>

    <ResponseField name="settle_duration" type="float">
      Duration in seconds that the DOM must remain stable (no mutations) to be considered "settled". Increase for slow animations. Defaults to 0.5.
    </ResponseField>

    <ResponseField name="timeout_s" type="float">
      Maximum seconds to wait for DOM to settle before timing out. Raises an error if timeout is reached. Defaults to 30.0.
    </ResponseField>
  </Tab>
</Tabs>

## Returns: `bool`

True if DOM settled successfully within timeout.

## Raises

### TimeoutError

If DOM doesn't settle within timeout\_s.


# wait_for_network_settled
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/helpers/functions/wait_for_network_settled



<Info>This function has multiple overloads</Info>

<Tabs>
  <Tab title="Wrapper Function Pattern">
    Executes a function and waits for network activity to settle before returning. This helper monitors network requests and waits until all network activity has completed.

    ## `wait_for_network_settled` vs `wait_for_dom_settled`

    * Use `wait_for_network_settled` when watching **network requests** (API calls, form submissions, resource loading)
    * Use [wait\_for\_dom\_settled](../functions/wait_for_dom_settled) when watching **DOM mutations** (elements added/removed/modified, loading spinners, dynamic content injection)

    ```python theme={null}
    async def wait_for_network_settled(
        *,
        page: Page,
        func: Callable[[], Awaitable[Any]],
        timeout_s: int,
        max_inflight_requests: int,
    ) -> Any
    ```

    This pattern executes a function and waits for network activity to settle before returning the result of the function.

    ## Examples

    <CodeGroup>
      ```python Wrapper with Inline Function theme={null}
      from typing import TypedDict
      from playwright.async_api import Page
      from intuned_browser import wait_for_network_settled
      class Params(TypedDict):
          pass
      async def automation(page: Page, params: Params, **_kwargs):
          await page.goto('https://sandbox.intuned.dev/infinite-scroll')
          # Execute action and wait for network to settle
          async def scroll_action():
              # scroll to load more content
              await page.evaluate("() => { window.scrollTo(0, document.body.scrollHeight); }")
              return "scrolled"
          result = await wait_for_network_settled(
              page=page,
              func=scroll_action,
              timeout_s=15,
              max_inflight_requests=0
          )
          print(result)  # "scrolled"
          return result
      ```

      ```python Click Link with Network Wait theme={null}
      from typing import TypedDict
      from playwright.async_api import Page
      from intuned_browser import wait_for_network_settled
      class Params(TypedDict):
          pass
      async def automation(page: Page, params: Params, **_kwargs):
          await page.goto('https://sandbox.intuned.dev/')
          # Click Object Extractors link and wait for page to load
          async def click_action():
              await page.get_by_text('Object Extractors').click()
          await wait_for_network_settled(
              page=page,
              func=click_action,
              timeout_s=10,
              max_inflight_requests=0
          )
          # Object Extractors page loaded
          title = await page.locator('div h2').inner_text()
          return title
      ```
    </CodeGroup>

    ## Arguments

    <ResponseField name="page" type="Page">
      Playwright Page object to monitor network activity on.
    </ResponseField>

    <ResponseField name="func" type="Callable[[], Any]">
      The async function to execute before waiting for network to settle. This function should contain the action that triggers network requests.
    </ResponseField>

    <ResponseField name="timeout_s" type="int">
      Maximum seconds to wait for network to settle. If timeout is reached, logs a warning and continues. Defaults to 30.
    </ResponseField>

    <ResponseField name="max_inflight_requests" type="int">
      Maximum number of ongoing requests to consider network as "settled". Defaults to 0 (waits for all requests).
    </ResponseField>
  </Tab>

  <Tab title="Decorator">
    Executes a function and waits for network activity to settle before returning. This helper monitors network requests and waits until all network activity has completed.

    ## `wait_for_network_settled` vs `wait_for_dom_settled`

    * Use `wait_for_network_settled` when watching **network requests** (API calls, form submissions, resource loading)
    * Use [wait\_for\_dom\_settled](../functions/wait_for_dom_settled) when watching **DOM mutations** (elements added/removed/modified, loading spinners, dynamic content injection)

    ```python theme={null}
    def wait_for_network_settled(
        func: Callable[..., Awaitable[Any]],
    ) -> Callable[..., Awaitable[Any]]
    ```

    This pattern decorates a function to automatically add network waiting functionality to the wrapped function.

    ## Examples

    <CodeGroup>
      ```python Simple Decorator theme={null}
      from typing import TypedDict
      from playwright.async_api import Page
      from intuned_browser import wait_for_network_settled
      class Params(TypedDict):
          pass
      async def automation(page: Page, params: Params, **_kwargs):
          await page.goto("https://sandbox.intuned.dev/load-more")
          # Decorator without arguments (uses timeout_s=30, max_inflight_requests=0)
          @wait_for_network_settled
          async def click_load_more(page):
              await page.locator("main main button").click()
          # Automatically waits for network to settle after clicking
          await click_load_more(page)
          # Network has settled, data is loaded
      ```
    </CodeGroup>

    ## Arguments

    <ResponseField name="func" type="Callable[[Page], Any]">
      The async function to decorate. Must accept a Page object as a parameter. If not provided, returns a parameterized decorator.
    </ResponseField>

    <ResponseField name="timeout_s" type="int">
      Maximum seconds to wait for network to settle. If timeout is reached, logs a warning and continues (doesn't raise an error). Defaults to 30.
    </ResponseField>

    <ResponseField name="max_inflight_requests" type="int">
      Maximum number of ongoing requests to consider network as "settled". Useful for pages with long-polling or streaming. Defaults to 0 (waits for all requests to complete).
    </ResponseField>
  </Tab>
</Tabs>

## Returns: `Any`

The return value of the executed function.


# Attachment
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/helpers/type-references/Attachment



Represents an uploaded file stored in AWS S3 with metadata and utility methods.

Provides a structured way to handle file information for files stored in S3, including methods for generating presigned URLs, serialization, and accessing file metadata.

```python theme={null}
class Attachment(BaseModel)
```

## Properties

<ParamField type="str">
  The name of the file in the S3 bucket
</ParamField>

<ParamField type="str">
  The key of the file in the S3 bucket
</ParamField>

<ParamField type="str">
  The S3 bucket name where the file is stored
</ParamField>

<ParamField type="str">
  The AWS region where the S3 bucket is located
</ParamField>

<ParamField type="str">
  Optional custom S3 endpoint URL. Defaults to None for standard AWS S3
</ParamField>

<ParamField type="str">
  A human-readable filename suggestion for downloads or display
</ParamField>

<ParamField type="AttachmentType">
  The type of the file
</ParamField>

## Methods

<AccordionGroup>
  <Accordion title="__json__">
    Returns a JSON-serializable dictionary representation of the file.

    **Returns:** `dict`

    Complete model data including all fields
  </Accordion>

  <Accordion title="to_dict">
    Converts the file metadata to a dictionary.

    **Returns:** `dict[str, str]`

    Dictionary with file\_name, key, bucket, region, endpoint, suggested\_file\_name, and file\_type
  </Accordion>

  <Accordion title="from_dict">
    Class method to create an Attachment instance from a dictionary.

    <ParamField type="optional[int]">
      URL expiration time in seconds. Defaults to 432000 (5 days)
    </ParamField>

    **Returns:** `str`

    Presigned URL for downloading the file
  </Accordion>

  <Accordion title="get_s3_key">
    Returns the full S3 URL for the file.

    **Returns:** `str`

    Complete S3 URL in format: [https://bucket.s3.region.amazonaws.com/filename](https://bucket.s3.region.amazonaws.com/filename)
  </Accordion>

  <Accordion title="get_file_path">
    Returns the file path/key within the S3 bucket.

    **Returns:** `str`

    The file\_name attribute (S3 object key)
  </Accordion>
</AccordionGroup>

## Examples

<CodeGroup>
  ```python Basic Usage theme={null}
  from intuned_browser import upload_file_to_s3, Attachment
  async def automation(page, params, **_kwargs):
      uploaded_file: Attachment = await upload_file_to_s3(
          file=my_file,
          configs=s3_config
      )

      # Access file properties
      print(uploaded_file.file_name)
      print(uploaded_file.suggested_file_name)
  ```

  ```python Working with Presigned URLs theme={null}
  from intuned_browser import upload_file_to_s3, Attachment
  async def automation(page, params, **_kwargs):
      uploaded_file: Attachment = await upload_file_to_s3(file=my_file)

      # Generate a presigned URL for temporary access
      download_url = await uploaded_file.get_signed_url(expiration=3600)  # 1 hour expiration

      # Get the permanent S3 URL
      s3_url = uploaded_file.get_s3_key()
  ```

  ```python Serialization theme={null}
  from intuned_browser import upload_file_to_s3, Attachment
  async def automation(page, params, **_kwargs):
      uploaded_file: Attachment = await upload_file_to_s3(file=my_file)

      # Convert to dictionary for storage or API responses
      file_dict = uploaded_file.to_dict()
      file_json = uploaded_file.__json__()
  ```
</CodeGroup>


# AttachmentType
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/helpers/type-references/AttachmentType



Union type representing the supported attachment file types.

Currently supported types:

* `"document"`: Document files (PDFs, Word docs, etc.)

```python theme={null}
class AttachmentType(str, Enum)
```


# FileType
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/helpers/type-references/FileType



A union type representing supported file types for upload operations in web automation.

This type alias standardizes the file types that can be uploaded to S3 storage,
providing flexibility for different upload scenarios from browser downloads to raw binary data.

Type variants:

* `Download`: Playwright Download object from browser download operations
* `bytes`: Raw binary file content

```python theme={null}
type FileType = Download | bytes
```

## Examples

<CodeGroup>
  ```python Using Download Object theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import download_file, upload_file_to_s3
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      # From a browser download
      download = await download_file(
          page,
          trigger="https://intuned-docs-public-images.s3.amazonaws.com/32UP83A_ENG_US.pdf"
      )
      uploaded = await upload_file_to_s3(file=download)
  ```

  ```python Using Bytes theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import upload_file_to_s3
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      # From raw bytes
      file_buffer = b"PDF content here..."
      uploaded = await upload_file_to_s3(
          file=file_buffer,
          file_name_override="generated.pdf"
      )
  ```
</CodeGroup>


# S3Configs
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/helpers/type-references/S3Configs



Configuration class for AWS S3 storage operations.

This class defines the configuration parameters needed to connect to and interact
with AWS S3 storage services. It supports both standard AWS S3 and S3-compatible
storage services through custom endpoints.

```python theme={null}
class S3Configs(BaseModel)
```

## Properties

<ParamField type="str">
  AWS access key ID for authentication. If None, will attempt to use default AWS credentials or environment variables.
</ParamField>

<ParamField type="str">
  AWS secret access key for authentication. If None, will attempt to use default AWS credentials or environment variables.
</ParamField>

<ParamField type="str">
  Name of the S3 bucket to store files in. Must be a valid S3 bucket name following AWS naming conventions.
</ParamField>

<ParamField type="str">
  AWS region where the S3 bucket is located. Examples: 'us-east-1', 'eu-west-1', 'ap-southeast-1'
</ParamField>

<ParamField type="str">
  Custom endpoint URL for S3-compatible storage services. Use this for services like MinIO, DigitalOcean Spaces, or other S3-compatible APIs. For standard AWS S3, leave this as None.
</ParamField>

## Examples

<CodeGroup>
  ```python Basic Configuration theme={null}
  from intuned_browser import S3Configs
  async def automation(page, params, **_kwargs):
      # Using explicit credentials
      s3_config = S3Configs(
          access_key="accessKeyId",
          secret_key="SecretAccessKeyId",
          bucket_name="my-app-uploads",
          region="us-east-1"
      )
  ```

  ```python Environment Variables Configuration theme={null}
  from intuned_browser import S3Configs
  async def automation(page, params, **_kwargs):
      # Credentials will be picked up from environment or IAM roles
      s3_config = S3Configs(
          bucket_name="my-app-uploads",
          region="us-west-2"
      )
  ```
</CodeGroup>


# Trigger
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/helpers/type-references/Trigger



A union type representing different methods to trigger file downloads in web automation.

This type alias standardizes how download operations can be initiated, providing
multiple approaches to suit different automation scenarios.

```python theme={null}
type Trigger = str | Locator | Callable[[Page], None] | Callable[[Page], Awaitable[None]]
```

Type Information:

* `str`: Direct URL string to download from
* `Locator`: Playwright Locator object pointing to a clickable download element
* `Callable[[Page], None]` | `Callable[[Page], Awaitable[None]]`: Custom async function that takes a Page and triggers the download

## Examples

<CodeGroup>
  ```python URL String Trigger theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import download_file
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      # Direct download from URL
      download = await download_file(
          page,
          trigger="https://intuned-docs-public-images.s3.amazonaws.com/32UP83A_ENG_US.pdf"
      )
  ```

  ```python Locator Trigger theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import download_file
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      await page.goto("https://sandbox.intuned.dev/pdfs")
      # Click a download button
      download = await download_file(
          page,
          trigger=page.locator("xpath=//tbody/tr[1]//*[name()='svg']")
      )
  ```

  ```python Callback Trigger theme={null}
  from typing import TypedDict
  from playwright.async_api import Page
  from intuned_browser import download_file
  class Params(TypedDict):
      pass
  async def automation(page: Page, params: Params, **_kwargs):
      await page.goto("https://sandbox.intuned.dev/pdfs")
      # Custom download logic
      download = await download_file(
          page,
          trigger=lambda p: p.locator("xpath=//tbody/tr[1]//*[name()='svg']").click()
      )
  ```
</CodeGroup>


# Python SDK
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/python/overview



Browser automation helpers for Python, built on [Playwright](https://playwright.dev/). This package provides utilities for common automation tasks—AI-powered data extraction, navigation with retries, pagination handling, and more.

## Installation

```bash theme={null}
pip install intuned-browser
```

<Note>
  When using [Intuned](https://intunedhq.com), this package is pre-installed in every Python project.
</Note>

## Quick example

```python theme={null}
from typing import TypedDict

from playwright.async_api import Page
from intuned_browser.ai import extract_structured_data, is_page_loaded
from intuned_browser.helpers import go_to_url

class Params(TypedDict):
    pass

async def automation(page: Page, params:Params, **kwargs):
    await go_to_url(page, "https://books.toscrape.com")
    loaded = await is_page_loaded(page)
    if not loaded:
      raise ValueError("Page is not loaded, can not extract data")

    # Extract all book listings from the page
    books = await extract_structured_data(
        source=page,
        data_schema={
            "type": "object",
            "properties": {
                "products": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "title": {"type": "string"},
                            "price": {"type": "string"}
                        }
                    }
                }
            }
        },
        prompt="Extract all book listings with their titles and prices",
        strategy="HTML",
        model="claude-haiku-4-5-20251001"
    )

    return books
```

## AI module

AI-powered utilities for data extraction and page analysis. These functions use AI and incur costs.

| Function                                                            | Description                                                        |
| ------------------------------------------------------------------- | ------------------------------------------------------------------ |
| [`extract_structured_data`](./ai/functions/extract_structured_data) | Extract structured data from pages using AI with schema validation |
| [`is_page_loaded`](./ai/functions/is_page_loaded)                   | Detect when a page has finished loading                            |

<Tip>AI functions support caching and matching to reduce costs.</Tip>

## Helpers module

| Function                                                                       | Description                                        |
| ------------------------------------------------------------------------------ | -------------------------------------------------- |
| [`go_to_url`](./helpers/functions/go_to_url)                                   | Navigate with automatic retries and error handling |
| [`wait_for_network_settled`](./helpers/functions/wait_for_network_settled)     | Wait for network requests to complete              |
| [`wait_for_dom_settled`](./helpers/functions/wait_for_dom_settled)             | Wait for DOM mutations to finish                   |
| [`scroll_to_load_content`](./helpers/functions/scroll_to_load_content)         | Load infinite-scroll content                       |
| [`click_until_exhausted`](./helpers/functions/click_until_exhausted)           | Click "Load More" buttons until all content loads  |
| [`extract_markdown`](./helpers/functions/extract_markdown)                     | Convert pages to markdown                          |
| [`download_file`](./helpers/functions/download_file)                           | Download files with different triggers             |
| [`save_file_to_s3`](./helpers/functions/save_file_to_s3)                       | Download and upload files to S3                    |
| [`upload_file_to_s3`](./helpers/functions/upload_file_to_s3)                   | Upload files with custom S3 configurations         |
| [`filter_empty_values`](./helpers/functions/filter_empty_values)               | Remove empty values from data                      |
| [`validate_data_using_schema`](./helpers/functions/validate_data_using_schema) | Validate data against schemas                      |
| [`process_date`](./helpers/functions/process_date)                             | Parse and normalize dates                          |
| [`sanitize_html`](./helpers/functions/sanitize_html)                           | Clean and sanitize HTML                            |
| [`resolve_url`](./helpers/functions/resolve_url)                               | Resolve relative URLs to absolute paths            |

## Requirements

* Python 3.8+
* Playwright (`pip install playwright && playwright install`)
* For AI functions: API key for your AI provider (set via environment variable or function parameter)


# extractStructuredData
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/ai/functions/extractStructuredData



<Tip>This function uses AI and incurs costs.</Tip>

<Info>This function has multiple overloads</Info>

<Tabs>
  <Tab title="Extract From Page or Locator">
    Extracts structured data from web pages using AI-powered content analysis.

    This function provides intelligent data extraction from web pages using various strategies
    including HTML parsing, image analysis, and Markdown conversion. Or by using Text or Image Content.
    It supports extraction from entire pages or specific elements, with built-in caching and retry mechanisms.

    ```typescript theme={null}
    export declare function extractStructuredData(options: {
      source: Page | Locator;
      dataSchema: JsonSchema | z.ZodSchema;
      prompt?: string;
      strategy?: "IMAGE" | "MARKDOWN" | "HTML";
      enableDomMatching?: boolean;
      enableCache?: boolean;
      maxRetries?: number;
      model?: string;
      apiKey?: string;
    }): Promise<any>;
    ```

    Extract data from web pages or specific elements using HTML, IMAGE, or MARKDOWN strategies with DOM matching support.

    ## Features and limitations

    **Features:**

    * **Smart caching:** Hashes inputs and uses [KV Cache](https://docs.intunedhq.com/docs/01-learn/recipes/kv-cache) for persistent storage
    * **DOM matching:** With `enableDomMatching=true`, values match DOM elements for smart caching
    * **Multiple strategies:** HTML, IMAGE, or MARKDOWN based on content type
    * **Flexible models:** Use any up-to-date model from Anthropic, OpenAI, or Google based on your needs

    **Limitations:**

    * **Model variability:** Quality varies by model—experiment to find the best fit
    * **DOM complexity:** Dynamic structures can affect caching and matching
    * **IMAGE strategy constraints:** Can't capture truncated or off-screen content
    * **Schema design:** Complex schemas may reduce accuracy

    ## Examples

    <CodeGroup>
      ```typescript Extract book details theme={null}
      import { extractStructuredData } from "@intuned/browser/ai";
      import { BrowserContext, Page } from "playwright";

      interface Params {}

      export default async function handler(
        params: Params,
        page: Page,
        context: BrowserContext
      ) {
        await page.goto(
          "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"
        );
        // This will extract the book details from the page, using the HTML strategy with the gpt-5.4-mini model.
        // The dataSchema is a JSON Schema object that defines the structure of the data to extract.
        // You can also use a Zod schema instead of a JSON Schema object.
        const book = await extractStructuredData({
          source: page,
          strategy: "HTML", // The HTML strategy is the default strategy and will be used if no strategy is provided.
          model: "gpt-5.4-mini",
          dataSchema: {
            type: "object",
            properties: {
              name: { type: "string" },
              price: { type: "string" },
              description: { type: "string" },
              inStock: { type: "string" },
              rating: { type: "string" },
            },
            required: ["name", "price"],
          },
          prompt: "Extract book details from this page",
          enableCache: true, // since this is true, the method will call AI for the first time, and then whenever you call this method it will return cached results as long as the DOM is the same.
          enableDomMatching: true, // since this is true, the method will return the results mapped to the DOM elements, you MUST enable cache for this to work.
          maxRetries: 3,
        });

        console.log(`Found book: ${book.name} - ${book.price}`);
      }
      ```

      ```typescript Extract all books listings theme={null}
      import { extractStructuredData } from "@intuned/browser/ai";
      import { BrowserContext, Page } from "playwright";

      interface Params {}

      export default async function handler(
        params: Params,
        page: Page,
        context: BrowserContext
      ) {
        await page.goto("https://books.toscrape.com/");
        // This will extract all the books listings from the page, using the HTML strategy with the claude-sonnet-4-5 model.
        // The dataSchema is a JSON Schema object that defines the structure of the data to extract.
        // You can also use a Zod schema instead of a JSON Schema object.
        const books = await extractStructuredData({
          source: page,
          strategy: "HTML",
          model: "claude-sonnet-4-5",
          dataSchema: {
            type: "object",
            properties: {
              products: {
                type: "array",
                items: {
                  type: "object",
                  properties: {
                    title: { type: "string" },
                    price: { type: "string" },
                    availability: { type: "string" },
                  },
                },
              },
            },
          },
          prompt: "Extract all book listings",
          enableCache: false, // In this example, we don't want to cache the extracted data, we want to extract the data every time.
        });

        for (const book of books.products) {
          console.log(`${book.title}: ${book.price}`);
        }
      }
      ```
    </CodeGroup>

    ## Arguments

    <ResponseField name="options" type="Object">
      Configuration object containing extraction parameters

      <Expandable title="properties">
        <ResponseField name="options.source" type="Page|Locator">
          Playwright Page object to extract data from the entire page or Locator object
          to extract data from a specific element.
        </ResponseField>

        <ResponseField name="options.dataSchema" type="JsonSchema|z.ZodSchema">
          Schema defining the structure of the data to extract. Can be a JSON Schema
          object or a Zod schema.
        </ResponseField>

        <ResponseField name="options.prompt" type="string">
          Optional prompt to guide the extraction process and provide more context.
          Defaults to undefined.
        </ResponseField>

        <ResponseField name="options.strategy" type="'HTML'|'IMAGE'|'MARKDOWN'">
          Type of extraction strategy: - **"HTML"** (default) - Best for text-heavy
          pages with structured content - **"IMAGE"** - Best for visual content, charts,
          or complex layouts - **"MARKDOWN"** - Best for article-style content with
          semantic structure
        </ResponseField>

        <ResponseField name="options.enableDomMatching" type="boolean">
          Whether to enable DOM element matching during extraction. You must enable
          cache for this to work. When enabled, extraction results are mapped to their
          corresponding DOM elements and returned with matched results. These results
          are intelligently cached, allowing subsequent extractions with minor DOM
          changes to utilize the cached data for improved performance. Defaults to
          false.
        </ResponseField>

        <ResponseField name="options.enableCache" type="boolean">
          Whether to enable caching of extraction results. Defaults to true.
        </ResponseField>

        <ResponseField name="options.maxRetries" type="number">
          Maximum number of retry attempts on failures. Failures can be validation
          errors, API errors, output errors, etc. Defaults to 3.
        </ResponseField>

        <ResponseField name="options.model" type="string">
          AI model to use for extraction. Accepts any model supported by Anthropic,
          OpenAI, or Google. Defaults to "claude-haiku-4-5-20251001".
        </ResponseField>

        <ResponseField name="options.apiKey" type="string">
          Optional API key for AI extraction (if provided, will not be billed to your
          account). Defaults to undefined.
        </ResponseField>
      </Expandable>
    </ResponseField>
  </Tab>

  <Tab title="Extract From Content">
    Extracts structured data from web pages using AI-powered content analysis.

    This function provides intelligent data extraction from web pages using various strategies
    including HTML parsing, image analysis, and Markdown conversion. Or by using Text or Image Content.
    It supports extraction from entire pages or specific elements, with built-in caching and retry mechanisms.

    ```typescript theme={null}
    export declare function extractStructuredData(options: {
      content: ContentItem[] | ContentItem;
      dataSchema: JsonSchema | z.ZodSchema;
      prompt?: string;
      maxRetries?: number;
      enableCache?: boolean;
      model?: string;
      apiKey?: string;
    }): Promise<any>;
    ```

    Extract data from text, image buffers, or image URLs without requiring a page source.

    ## Features and limitations

    **Features:**

    * **Smart caching:** Hashes content and uses [KV Cache](https://docs.intunedhq.com/docs/01-learn/recipes/kv-cache) for persistent storage
    * **Multiple content items:** Combine text, images (buffer or URL) for comprehensive extraction
    * **Flexible models:** Use any up-to-date model from Anthropic, OpenAI, or Google based on your needs

    **Limitations:**

    * **Model variability:** Quality varies by model—experiment to find the best fit
    * **Schema design:** Complex schemas may reduce accuracy
    * **Content quality:** Requires meaningful, contextual content for accurate extraction—sparse or ambiguous content produces poor results

    ## Examples

    <CodeGroup>
      ```typescript Basic Text Content Extraction theme={null}
      import { extractStructuredData, TextContentItem } from "@intuned/browser/ai";
      import { BrowserContext, Page } from "playwright";

      interface Params {}

      export default async function handler(
        params: Params,
        page: Page,
        context: BrowserContext
      ) {
        // This will extract the person information from the text, using the gpt-5.4-mini model.
        const textContent: TextContentItem = {
          type: "text",
          data: "John Doe, age 30, works as a Software Engineer at Tech Corp",
        };

        const person = await extractStructuredData({
          content: textContent,
          model: "gpt-5.4-mini",
          dataSchema: {
            type: "object",
            properties: {
              name: { type: "string" },
              age: { type: "number" },
              occupation: { type: "string" },
              company: { type: "string" },
            },
            required: ["name"],
          },
          prompt: "Extract person information from the text",
        });

        console.log(`Found person: ${person.name}, ${person.age} years old`);
      }
      ```

      ```typescript List Extraction from Text Content theme={null}
      import { extractStructuredData, TextContentItem } from "@intuned/browser/ai";
      import { BrowserContext, Page } from "playwright";

      interface Params {}

      export default async function handler(
        params: Params,
        page: Page,
        context: BrowserContext
      ) {
        const textContent: TextContentItem = {
          type: "text",
          data: "iPhone 15 - $999, Samsung Galaxy - $899, Pixel 8 - $699",
        };

        const products = await extractStructuredData({
          content: textContent,
          model: "gpt-5.4-mini",
          dataSchema: {
            type: "object",
            properties: {
              products: {
                type: "array",
                items: {
                  type: "object",
                  properties: {
                    name: { type: "string" },
                    price: { type: "string" },
                  },
                },
              },
            },
          },
          prompt: "Extract all products",
        });

        for (const product of products.products) {
          console.log(`${product.name}: ${product.price}`);
        }
      }
      ```
    </CodeGroup>

    ## Arguments

    <ResponseField name="options" type="Object">
      Configuration object containing extraction parameters

      <Expandable title="properties">
        <ResponseField name="options.content" type="Array<ContentItem>|ContentItem">
          Content to extract data from - can be a single content item or array of
          [ContentItem](../type-references/ContentItem).
        </ResponseField>

        <ResponseField name="options.dataSchema" type="JsonSchema|z.ZodSchema">
          Schema defining the expected structure of the extracted data. Can be a JSON
          Schema object or a Zod schema.
        </ResponseField>

        <ResponseField name="options.prompt" type="string">
          Optional prompt to guide the extraction process and provide more context.
          Defaults to undefined.
        </ResponseField>

        <ResponseField name="options.maxRetries" type="number">
          Maximum number of retry attempts on failures. Failures can be validation
          errors, API errors, output errors, etc. Defaults to 3.
        </ResponseField>

        <ResponseField name="options.enableCache" type="boolean">
          Whether to enable caching of the extracted data. Defaults to true.
        </ResponseField>

        <ResponseField name="options.model" type="string">
          AI model to use for extraction. Accepts any model supported by Anthropic,
          OpenAI, or Google. Defaults to "claude-haiku-4-5-20251001".
        </ResponseField>

        <ResponseField name="options.apiKey" type="string">
          Optional API key for AI extraction (if provided, will not be billed to your
          account). Defaults to undefined.
        </ResponseField>
      </Expandable>
    </ResponseField>
  </Tab>
</Tabs>

## Returns: `Promise<any>`

The extracted structured data conforming to the provided schema.


# isPageLoaded
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/ai/functions/isPageLoaded



<Tip>This function uses AI and incurs costs.</Tip>

Uses AI vision to determine if a webpage has finished loading by analyzing a screenshot.
Detects loading spinners, blank content, or incomplete page states.

```typescript theme={null}
export declare function isPageLoaded(input: {
  page: Page;
  timeoutInMs?: number;
  model?: string;
  apiKey?: string;
}): Promise<boolean>;
```

## Examples

<CodeGroup>
  ```typescript Check Page Loading theme={null}
  import { isPageLoaded } from "@intuned/browser/ai";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    // Wait for page to finish loading
    await page.goto("https://sandbox.intuned.dev/");

    const pageLoaded = await isPageLoaded({ page });
    if (pageLoaded) {
      // Continue with scraping or interactions
      console.log("Page is loaded");
    } else {
      // Wait longer or retry
      await page.waitForTimeout(5000);
    }
  }
  ```

  ```typescript Loading Loop theme={null}
  import { isPageLoaded } from "@intuned/browser/ai";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    // Keep checking until page loads
    await page.goto("https://example.com");
    let attempts = 0;
    while (attempts < 10) {
      // We will retry up to 10 times with a 2-second delay between attempts.
      const pageLoaded = await isPageLoaded({
        page,
        model: "claude-sonnet-4-5",
        timeoutInMs: 5000,
      });
      if (pageLoaded) break; // If the page is loaded, break the loop.

      await page.waitForTimeout(2000); // Wait for 2 seconds before the next attempt.
      attempts++;
    }
  }
  ```
</CodeGroup>

## Arguments

<ResponseField name="input" type="Object">
  Input object containing the page to check

  <Expandable title="properties">
    <ResponseField name="input.page" type="Page">
      The Playwright page to check
    </ResponseField>

    <ResponseField name="input.timeoutInMs" type="number">
      Screenshot timeout in milliseconds. Defaults to 10000
    </ResponseField>

    <ResponseField name="input.model" type="string">
      AI model to use for the check. Accepts any vision-capable model supported by
      Anthropic, OpenAI, or Google. Defaults to "gpt-5-mini-2025-08-07".
    </ResponseField>

    <ResponseField name="input.apiKey" type="string">
      Optional API key for the AI call.
    </ResponseField>
  </Expandable>
</ResponseField>

## Returns: `Promise<boolean>`

Promise resolving to true if page is loaded, false if still loading.


# ContentItem
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/ai/type-references/ContentItem



A union type representing content items for AI data extraction from various content types.

This type alias defines the complete set of content types supported by the content-based
extractStructuredData function for extracting data from text, image buffers, or image URLs
without requiring a page source.

**Type variants:**

* `TextContentItem`: [TextContentItem](../type-references/TextContentItem) for text data extraction
* `ImageBufferContentItem`: [ImageBufferContentItem](../type-references/ImageBufferContentItem) for image data stored as Buffer
* `ImageUrlContentItem`: [ImageUrlContentItem](../type-references/ImageUrlContentItem) for image data accessible via URL

```typescript theme={null}
export type ContentItem = | TextContentItem
  | ImageBufferContentItem
  | ImageUrlContentItem;
```

## Examples

<CodeGroup>
  ```typescript Text Content theme={null}
  import { TextContentItem } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    const textContent: TextContentItem = {
      type: "text",
      data: "John Doe, age 30, works as a Software Engineer at Tech Corp",
    };
  }
  ```

  ```typescript Image Buffer Content theme={null}
  import { ImageBufferContentItem } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    // Assuming you have image data as Buffer
    const imageData = fs.readFileSync("image.png");

    const imageContent: ImageBufferContentItem = {
      type: "image-buffer",
      image_type: "png",
      data: imageData,
    };
  }
  ```

  ```typescript Image URL Content theme={null}
  import { ImageUrlContentItem } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    const imageContent: ImageUrlContentItem = {
      type: "image-url",
      image_type: "jpeg",
      data: "https://example.com/image.jpg",
    };
  }
  ```
</CodeGroup>


# ImageBufferContentItem
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/ai/type-references/ImageBufferContentItem



Image buffer content item for content-based extraction.

```typescript theme={null}
export interface ImageBufferContentItem {
  type: "image-buffer";
  image_type: "png" | "jpeg" | "gif" | "webp";
  data: Buffer;
}
```

## Properties

<ParamField type="'image-buffer'">
  The type of the content item, which is always "image-buffer".
</ParamField>

<ParamField type="'png' | 'jpeg' | 'gif' | 'webp'">
  The image format (e.g., "png", "jpeg", "gif", "webp").
</ParamField>

<ParamField type="Buffer">
  The Buffer containing the raw image data.
</ParamField>


# ImageUrlContentItem
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/ai/type-references/ImageUrlContentItem



Image URL content item for content-based extraction.

```typescript theme={null}
export interface ImageUrlContentItem {
  type: "image-url";
  image_type: "png" | "jpeg" | "gif" | "webp";
  data: string;
}
```

## Properties

<ParamField type="'image-url'">
  The type of the content item, which is always "image-url".
</ParamField>

<ParamField type="'png' | 'jpeg' | 'gif' | 'webp'">
  The image format (e.g., "png", "jpeg", "gif", "webp").
</ParamField>

<ParamField type="string">
  The URL of the image.
</ParamField>


# TextContentItem
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/ai/type-references/TextContentItem



Text content item for content-based extraction.

```typescript theme={null}
export interface TextContentItem {
  type: "text";
  data: string;
}
```

## Properties

<ParamField type="'text'">
  The type of the content item, which is always "text".
</ParamField>

<ParamField type="string">
  The text data to extract from.
</ParamField>


# clickUntilExhausted
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/clickUntilExhausted



Repeatedly click a button until no new content appears or max clicks reached.

This function is useful for "Load More" buttons or paginated content where you need to
keep clicking until all content is loaded. It provides several stopping conditions:

* Button becomes invisible/disabled
* Maximum number of clicks reached
* No change detected in container content (when containerLocator is provided)

```typescript theme={null}
export declare function clickUntilExhausted(input: {
  page: Page;
  buttonLocator: Locator;
  heartbeat?: CallableFunction;
  containerLocator?: Locator;
  maxClicks?: number;
  clickDelay?: number;
  noChangeThreshold?: number;
}): Promise<void>;
```

## Examples

<CodeGroup>
  ```typescript Load All Items theme={null}
  import { clickUntilExhausted } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    await page.goto("https://sandbox.intuned.dev/load-more");
    const loadMoreButton = page.locator("main main button"); // Select the main button in the main content area.

    // Click until button disappears or is disabled
    await clickUntilExhausted({
      page,
      buttonLocator: loadMoreButton,
      maxClicks: 20,
    });
    // Will keep clicking the button until the button disappears or is disabled or the maxClicks is reached.
  }
  ```

  ```typescript Track Container Changes theme={null}
  import { clickUntilExhausted } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    await page.goto("https://sandbox.intuned.dev/load-more");
    const loadMoreButton = page.locator("aside button"); // Select the button in the sidebar.
    const container = page.locator(
      'xpath=//*[@id="root"]/div[1]/main/slot/div/aside/div/div/slot/slot'
    ); // Watch the sidebar container to detect changes.
    // This will count the elements under the container given before each click and after, if the count is the same, the function will stop.
    let clickCount = 0;
    await clickUntilExhausted({
      page,
      buttonLocator: loadMoreButton,
      containerLocator: container,
      heartbeat: () => {
        clickCount++;
        console.log(`Clicked ${clickCount} times`);
      },
      maxClicks: 30,
      clickDelay: 0.5,
      noChangeThreshold: 0,
    });
    // Will keep clicking the button until the button disappears or is disabled or the maxClicks is reached or no more content is loaded.
  }
  ```
</CodeGroup>

## Arguments

<ResponseField name="input" type="Object">
  Configuration options

  <Expandable title="properties">
    <ResponseField name="input.page" type="Page">
      Playwright Page object
    </ResponseField>

    <ResponseField name="input.buttonLocator" type="Locator">
      Locator for the button to click repeatedly
    </ResponseField>

    <ResponseField name="input.heartbeat" type="CallableFunction">
      Optional callback invoked after each click
    </ResponseField>

    <ResponseField name="input.containerLocator" type="Locator">
      Optional content container to detect changes
    </ResponseField>

    <ResponseField name="input.maxClicks" type="number">
      Maximum number of times to click the button. Defaults to 50.
    </ResponseField>

    <ResponseField name="input.clickDelay" type="number">
      Delay after each click (in seconds). Defaults to 0.5.
    </ResponseField>

    <ResponseField name="input.noChangeThreshold" type="number">
      Minimum change in content size to continue clicking. Defaults to 0.
    </ResponseField>
  </Expandable>
</ResponseField>

## Returns: `Promise<void>`

Promise that resolves when clicking is exhausted


# downloadFile
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/downloadFile



Downloads a file from a web page using various trigger methods. This function provides three flexible ways to initiate file downloads:

* **URL**: Creates a new page, navigates to the URL, waits for download, then automatically closes the page. Ideal for direct download links.
* **Locator**: Uses the current page to click the element and capture the resulting download. Perfect for download buttons or interactive elements.
* **Callback**: Executes the provided function with the page object and captures the first triggered download. Offers maximum flexibility for complex download scenarios.

```typescript theme={null}
export declare function downloadFile(input: {
  page: Page;
  trigger: Trigger;
  timeoutInMs?: number;
}): Promise<Download>;
```

## Examples

<CodeGroup>
  ```typescript Download from direct URL theme={null}
  import { downloadFile } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    // Download from a direct URL, this will open the url and automatically download the content in it.
    const download = await downloadFile({
      page,
      trigger:
        "https://intuned-docs-public-images.s3.amazonaws.com/32UP83A_ENG_US.pdf",
    });
    const file_name = download.suggestedFilename();
    return file_name;
  }
  ```

  ```typescript Locator Trigger theme={null}
  import { downloadFile } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    await page.goto("https://sandbox.intuned.dev/pdfs");
    const download = await downloadFile({
      page,
      trigger: page.locator("xpath=//tbody/tr[1]//*[name()='svg']"),
    });
    const file_name = download.suggestedFilename();
    return file_name;
  }
  ```

  ```typescript Callback Trigger theme={null}
  import { downloadFile } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    await page.goto("https://sandbox.intuned.dev/pdfs");
    const download = await downloadFile({
      page,
      trigger: async (page) => {
        await page.locator("xpath=//tbody/tr[1]//*[name()='svg']").click();
      },
    });
    const file_name = download.suggestedFilename();
    return file_name;
  }
  ```
</CodeGroup>

## Arguments

<ResponseField name="input" type="Object">
  Configuration object for the download operation

  <Expandable title="properties">
    <ResponseField name="input.page" type="Page">
      The Playwright Page object to use for the download
    </ResponseField>

    <ResponseField name="input.trigger" type="Trigger">
      The [Trigger](../type-references/Trigger) method to initiate the download
    </ResponseField>

    <ResponseField name="input.timeoutInMs" type="number">
      Maximum time in milliseconds to wait for the download to trigger. Defaults to
      5000\.
    </ResponseField>
  </Expandable>
</ResponseField>

## Returns: `Promise<Download>`

Promise that resolves to a [Playwright Download object](https://playwright.dev/docs/api/class-download)


# extractMarkdown
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/extractMarkdown



Converts HTML content from a Playwright Page or Locator to semantic markdown format.

```typescript theme={null}
export declare function extractMarkdown(input: {
  source: Page | Locator;
}): Promise<string>;
```

## Examples

<CodeGroup>
  ```typescript Extract Markdown from Locator theme={null}
  import { extractMarkdown } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    await page.goto("https://books.toscrape.com/");
    const headerLocator = page.locator("h1").first(); // First title on the page
    const markdown = await extractMarkdown({ source: headerLocator }); // Extract markdown from the first title
    console.log(markdown);
    return markdown;
  }
  ```

  ```typescript Extract Markdown from Page theme={null}
  import { extractMarkdown } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    await page.goto("https://sandbox.intuned.dev/pdfs");
    const markdown = await extractMarkdown({ source: page });
    console.log(markdown);
    return markdown;
  }
  ```
</CodeGroup>

## Arguments

<ResponseField name="input" type="Object">
  The input object containing the source of the HTML content

  <Expandable title="properties">
    <ResponseField name="input.source" type="Page | Locator">
      The source of the HTML content. When a Page is provided, extracts from the entire page. When a Locator is provided, extracts from that specific element.
    </ResponseField>
  </Expandable>
</ResponseField>

## Returns: `Promise<string>`

Promise that resolves to the markdown representation of the HTML content


# filterEmptyValues
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/filterEmptyValues



Recursively filters out empty values from nested objects and arrays.

This function removes the following empty values:

* `null` and `undefined` values
* Empty strings (after trimming whitespace)
* Empty arrays
* Empty objects
* Arrays and objects that become empty after filtering their contents

```typescript theme={null}
export declare function filterEmptyValues<T>(input: { data: T }): T;
```

## Examples

<CodeGroup>
  ```typescript Basic Usage theme={null}
  import { filterEmptyValues } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    // Filter empty values from dictionary
    const result1 = filterEmptyValues({ data: { a: "", b: "hello", c: null } });
    // Output: { b: "hello" }
    console.log(result1);

    // Filter empty values from list
    const result2 = filterEmptyValues({ data: [1, "", null, [2, ""]] });
    // Output: [1, [2]]
    console.log(result2);

    // Filter nested structures
    const result3 = filterEmptyValues({
      data: { users: [{ name: "" }, { name: "John" }] },
    });
    // Output: { users: [{ name: "John" }] }
    console.log(result3);
    return "All data filtered successfully";
  }
  ```
</CodeGroup>

## Arguments

<ResponseField name="input" type="Object">
  The input object containing the data to filter

  <Expandable title="properties">
    <ResponseField name="input.data" type="T">
      The data structure to filter (object, array, or any other type)
    </ResponseField>
  </Expandable>
</ResponseField>

## Returns: `T`

Filtered data structure with empty values removed


# goToUrl
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/goToUrl



Navigates to a specified URL with enhanced reliability features including automatic retries with exponential backoff,
intelligent timeout handling, and optional AI-powered loading verification.

This function handles common navigation challenges by automatically retrying failed requests, detecting navigation hangs, and ensuring the page reaches a truly idle state.

This method can also use AI vision to verify the page is fully loaded by checking for loading spinners, blank content, or incomplete states.

```typescript theme={null}
export declare function goToUrl(input: {
  page: Page;
  url: string;
  timeoutInMs?: number;
  retries?: number;
  throwOnTimeout?: boolean;
  waitForLoadState?: "load" | "domcontentloaded" | "networkidle" | "commit";
  waitForLoadingStateUsingAi?: boolean;
  model?: string;
  apiKey?: string;
}): Promise<void>;
```

## Examples

<CodeGroup>
  ```typescript Without options theme={null}
  import { goToUrl } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    await goToUrl({
      page,
      url: "https://sandbox.intuned.dev/",
    });
    // At this point, goToUrl has waited for the page to be loaded and the network requests to be settled.
  }
  ```

  ```typescript With options theme={null}
  import { goToUrl } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    await goToUrl({
      page,
      url: "https://intunedhq.com",
      waitForLoadState: "domcontentloaded", // Faster than "load" state. The function automatically waits for the page to settle.
      throwOnTimeout: true,
      timeoutInMs: 10000,
      retries: 3,
    });
    // At this point, DOM content is loaded and goToUrl has waited for network requests to settle.
  }
  ```

  ```typescript With AI Loading Detection theme={null}
  import { goToUrl } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    await goToUrl({
      page,
      url: "https://intunedhq.com",
      waitForLoadingStateUsingAi: true,
      model: "gpt-5.4-mini",
    });
    // The page is loaded and ready to use.
    // If the AI check fails, the method won't throw even if throwOnTimeout is true.
    // It only throws if the page times out reaching the default load state and throwOnTimeout is true.
  }
  ```
</CodeGroup>

## Arguments

<ResponseField name="input" type="Object">
  The input object containing the page and url.

  <Expandable title="properties">
    <ResponseField name="input.page" type="Page">
      The Playwright page object to navigate.
    </ResponseField>

    <ResponseField name="input.url" type="string">
      The URL to navigate to.
    </ResponseField>

    <ResponseField name="input.timeoutInMs" type="number">
      Maximum time in milliseconds to wait for navigation. Defaults to 30000.
    </ResponseField>

    <ResponseField name="input.retries" type="number">
      Number of retry attempts with exponential backoff (factor: 2). Defaults to 3.
    </ResponseField>

    <ResponseField name="input.waitForLoadState" type="'load'|'domcontentloaded'|'networkidle'|'commit'">
      When to consider navigation succeeded. Defaults to `"load"`
    </ResponseField>

    <ResponseField name="input.throwOnTimeout" type="boolean">
      Whether to throw an error if navigation times out. When true, the function
      throws an error on navigation timeout. Defaults to false.
    </ResponseField>

    <ResponseField name="input.waitForLoadingStateUsingAi" type="boolean">
      When true, uses AI vision to verify the page is fully loaded by checking for
      loading spinners, blank content, or incomplete states. Retries up to 3 times
      with 5-second delays. Defaults to false Check
      [isPageLoaded](../../ai/functions/isPageLoaded) for more details on the AI
      loading verification.
    </ResponseField>

    <ResponseField name="input.model" type="string">
      AI model to use for loading verification. Defaults to
      `"gpt-5-mini-2025-08-07"`
    </ResponseField>

    <ResponseField name="input.apiKey" type="string">
      Optional API key for the AI check.
    </ResponseField>
  </Expandable>
</ResponseField>

## Returns: `Promise<void>`

Promise that resolves when navigation completes. If the operation fails and `throwOnTimeout` is false, resolves without error


# processDate
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/processDate



Parses various date string formats into Date objects, returning only the date part with time set to midnight.
This utility function provides robust date parsing capabilities for a wide range of common formats.

```typescript theme={null}
export declare function processDate(input: { date: string }): Date | null;
```

## Key features

* Returns only the date part (year, month, day)
* Time is always set to 00:00:00
* Supports multiple international formats
* Handles timezones and AM/PM formats

## Supported formats

The function handles these date format categories:

### Standard date formats

* `DD/MM/YYYY`: "22/11/2024", "13/12/2024"
* `MM/DD/YYYY`: "01/17/2025", "10/25/2024"
* Single-digit variants: "8/16/2019", "9/28/2024"

### Date-time combinations

* With 24-hour time: "22/11/2024 21:19:05"
* With AM/PM: "12/09/2024 9:00 AM"
* With dash separator: "12/19/2024 - 2:00 PM"

### Timezone support

* With timezone abbreviations: "10/23/2024 12:06 PM CST"
* With timezone offset: "01/17/2025 3:00:00 PM CT"

### Text month formats

* Short month: "5 Dec 2024", "11 Sep 2024"
* With time: "5 Dec 2024 8:00 AM PST"
* Full month: "November 14, 2024", "January 31, 2025, 5:00 pm"

## Examples

<CodeGroup>
  ```typescript Basic Usage theme={null}
  import { processDate } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    // Basic date string
    const date1 = processDate({ date: "22/11/2024" });
    console.log(date1); // 2024-11-22 00:00:00

    // Date with time (time is ignored)
    const date2 = processDate({ date: "5 Dec 2024 8:00 AM PST" });
    console.log(date2); // 2024-12-05 00:00:00
  }
  ```

  ```typescript Invalid Date theme={null}
  import { processDate } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    // Invalid date returns null
    const invalidDate = processDate({ date: "invalid date" });
    console.log(invalidDate); // will return null.
    if (invalidDate === null) {
      throw new Error("Invalid date");
    }
    return "should not reach here";
  }
  ```
</CodeGroup>

## Arguments

<ResponseField name="input" type="Object">
  The input object containing the date to process

  <Expandable title="properties">
    <ResponseField name="input.date" type="string">
      A string containing a date in various possible formats
    </ResponseField>
  </Expandable>
</ResponseField>

## Returns: `Date | any`

Returns a `Date` object with only date components preserved (year, month, day), time always set to 00:00:00, or `null` if parsing fails


# resolveUrl
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/resolveUrl



<Info>This function has multiple overloads</Info>

<Tabs>
  <Tab title="Base URL String">
    Converts any URL source to an absolute, properly encoded URL.

    ```typescript theme={null}
    export declare function resolveUrl(input: {
      url: string;
      baseUrl: string;
    }): Promise<string>;
    ```

    Combines a relative URL with a base URL string. Use when you have an explicit base URL string to resolve relative paths against.

    ## Examples

    <CodeGroup>
      ```typescript Resolve from Base URL String theme={null}
      import { resolveUrl } from "@intuned/browser";
      import { BrowserContext, Page } from "playwright";

      interface Params {}

      export default async function handler(
        params: Params,
        page: Page,
        context: BrowserContext
      ) {
        // Resolve from base URL string
        const absoluteUrl = await resolveUrl({
          url: "/lists/table",
          baseUrl: "https://sandbox.intuned.dev",
        });
        // Returns: "https://sandbox.intuned.dev/lists/table"
        console.log(absoluteUrl);
        return absoluteUrl;
      }
      ```
    </CodeGroup>

    ## Arguments

    <ResponseField name="input" type="Object">
      Configuration object with different properties based on the overload

      <Expandable title="properties">
        <ResponseField name="input.url" type="string">
          The relative or absolute URL to resolve
        </ResponseField>

        <ResponseField name="input.baseUrl" type="string">
          Base URL string to resolve relative URLs against
        </ResponseField>
      </Expandable>
    </ResponseField>
  </Tab>

  <Tab title="Current Page's URL">
    Converts any URL source to an absolute, properly encoded URL.

    ```typescript theme={null}
    export declare function resolveUrl(input: {
      url: string;
      page: Page;
    }): Promise<string>;
    ```

    Uses the current page's URL as the base URL. Use when resolving URLs relative to the current page.

    ## Examples

    <CodeGroup>
      ```typescript Resolve from the Current Page's URL theme={null}
      import { resolveUrl } from "@intuned/browser";
      import { BrowserContext, Page } from "playwright";

      interface Params {}

      export default async function handler(
        params: Params,
        page: Page,
        context: BrowserContext
      ) {
        await page.goto("https://sandbox.intuned.dev/");
        // Resolve from the current page's URL
        const absoluteUrl = await resolveUrl({
          url: "/lists/table",
          page: page,
        });
        // Returns: "https://sandbox.intuned.dev/lists/table"
        console.log(absoluteUrl);
        return absoluteUrl;
      }
      ```
    </CodeGroup>

    ## Arguments

    <ResponseField name="input" type="Object">
      Configuration object with different properties based on the overload

      <Expandable title="properties">
        <ResponseField name="input.url" type="string">
          The relative or absolute URL to resolve
        </ResponseField>

        <ResponseField name="input.page" type="Page">
          Playwright Page object to extract base URL from. The current page URL will be used as the base URL
        </ResponseField>
      </Expandable>
    </ResponseField>
  </Tab>

  <Tab title="Anchor Elements">
    Converts any URL source to an absolute, properly encoded URL.

    ```typescript theme={null}
    export declare function resolveUrl(input: { url: Locator }): Promise<string>;
    ```

    Extracts the href attribute from a Playwright Locator pointing to an anchor element. Use when extracting and resolving URLs from anchor (`<a>`) elements.

    ## Examples

    <CodeGroup>
      ```typescript Resolve from Anchor Element theme={null}
      import { resolveUrl } from "@intuned/browser";
      import { BrowserContext, Page } from "playwright";

      interface Params {}

      export default async function handler(
        params: Params,
        page: Page,
        context: BrowserContext
      ) {
        await page.goto("https://sandbox.intuned.dev/");
        // Resolve from Anchor Element
        const absoluteUrl = await resolveUrl({
          url: page.locator("xpath=//a[normalize-space()='Steps Form']"),
        });
        // Returns: "https://sandbox.intuned.dev/steps-form"
        console.log(absoluteUrl);
        return absoluteUrl;
      }
      ```
    </CodeGroup>

    ## Arguments

    <ResponseField name="input" type="Object">
      Configuration object with different properties based on the overload

      <Expandable title="properties">
        <ResponseField name="input.url" type="Locator">
          Playwright Locator pointing to an anchor element. The href attribute will be extracted and resolved relative to the current page.
        </ResponseField>
      </Expandable>
    </ResponseField>
  </Tab>
</Tabs>

## Returns: `Promise<string>`

Promise that resolves to the absolute, properly encoded URL string


# sanitizeHtml
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/sanitizeHtml



Sanitizes and cleans HTML content by removing unwanted elements, attributes, and whitespace.
Provides fine-grained control over each cleaning operation through configurable options.

```typescript theme={null}
export declare function sanitizeHtml(options: SanitizeHtmlOptions): string;
```

## Examples

<CodeGroup>
  ```typescript Basic Sanitization theme={null}
  import { BrowserContext, Page } from "playwright";
  import { sanitizeHtml } from "@intuned/browser";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    await page.goto("https://books.toscrape.com");
    const firstRow = page.locator("ol.row").locator("li").first();
    // Get the HTML of the first row.
    const html = await firstRow.innerHTML();
    // Sanitize the HTML.
    const sanitizedHtml = sanitizeHtml({ html });
    // Log the sanitized HTML.
    console.log(sanitizedHtml);
    // Return the sanitized HTML.
    return sanitizedHtml;
  }
  ```
</CodeGroup>

## Arguments

<ResponseField name="options" type="SanitizeHtmlOptions">
  Configuration options for sanitization

  <Expandable title="properties">
    <ResponseField name="options.html" type="string">
      The HTML content to sanitize
    </ResponseField>

    <ResponseField name="options.removeScripts" type="boolean">
      Remove all `<script>` elements. Defaults to true.
    </ResponseField>

    <ResponseField name="options.removeStyles" type="boolean">
      Remove all `<style>` elements. Defaults to true.
    </ResponseField>

    <ResponseField name="options.removeSvgs" type="boolean">
      Remove all `<svg>` elements. Defaults to true.
    </ResponseField>

    <ResponseField name="options.removeComments" type="boolean">
      Remove HTML comments. Defaults to true.
    </ResponseField>

    <ResponseField name="options.removeLongAttributes" type="boolean">
      Remove attributes longer than maxAttributeLength. Defaults to true.
    </ResponseField>

    <ResponseField name="options.maxAttributeLength" type="number">
      Maximum length for attributes before removal. Defaults to 500.
    </ResponseField>

    <ResponseField name="options.preserveAttributes" type="Array<string>">
      List of attribute names to always preserve. Defaults to \["class", "src"].
    </ResponseField>

    <ResponseField name="options.removeEmptyTags" type="boolean">
      Remove empty tags (except preserved ones). Defaults to true.
    </ResponseField>

    <ResponseField name="options.preserveEmptyTags" type="Array<string>">
      List of tag names to preserve even when empty. Defaults to \["img"].
    </ResponseField>

    <ResponseField name="options.minifyWhitespace" type="boolean">
      Remove extra whitespace between tags and empty lines. Defaults to true.
    </ResponseField>
  </Expandable>
</ResponseField>

## Returns: `string`

The sanitized HTML string


# saveFileToS3
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/saveFileToS3



Downloads a file from a web page and automatically uploads it to AWS S3 storage in a single operation.

Combines [downloadFile](./downloadFile) (for trigger methods) and [uploadFileToS3](./uploadFileToS3)
(for S3 configuration), providing a streamlined workflow for capturing and storing files.

```typescript theme={null}
export declare function saveFileToS3(input: {
  page: Page;
  trigger: Trigger;
  timeoutInMs?: number;
  configs?: S3Configs;
  fileNameOverride?: string;
  contentType?: string;
}): Promise<Attachment>;
```

## Examples

<CodeGroup>
  ```typescript URL Trigger theme={null}
  import { saveFileToS3 } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    const uploadedFile = await saveFileToS3({
      page,
      trigger:
        "https://intuned-docs-public-images.s3.amazonaws.com/27UP600_27UP650_ENG_US.pdf",
      configs: {
        bucket: "document-storage",
        region: "us-east-1",
        accessKeyId: "accessKeyId",
        secretAccessKey: "SecretAccessKeyId",
      },
      fileNameOverride: "reports/monthly-report.pdf",
    });
    return { file: uploadedFile };
  }
  ```

  ```typescript Locator Trigger theme={null}
  import { saveFileToS3 } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    await page.goto("https://sandbox.intuned.dev/pdfs");
    const uploadedFile = await saveFileToS3({
      page,
      trigger: page.locator("xpath=//tbody/tr[1]//*[name()='svg']"),
      timeoutInMs: 10000,
    });
    const downloadUrl = await uploadedFile.getSignedUrl(7200); // 2 hours
    console.log(`Temporary access: ${downloadUrl}`);
  }
  ```
</CodeGroup>

## Arguments

<ResponseField name="input" type="Object">
  Configuration object for the download and upload operation

  <Expandable title="properties">
    <ResponseField name="input.page" type="Page">
      The Playwright Page object to use for downloading
    </ResponseField>

    <ResponseField name="input.trigger" type="Trigger">
      The [Trigger](../type-references/Trigger) method to initiate the download
    </ResponseField>

    <ResponseField name="input.timeoutInMs" type="number">
      Maximum time in milliseconds to wait for download to start. Defaults to 5000.
    </ResponseField>

    <ResponseField name="input.configs" type="S3Configs">
      Optional [S3Configs](../type-references/S3Configs) to customize the S3 upload.
      If not provided, uses environment variables (AWS\_ACCESS\_KEY\_ID,
      AWS\_SECRET\_ACCESS\_KEY, AWS\_REGION, AWS\_ENDPOINT\_URL, AWS\_BUCKET). If
      environment variables aren't set, uses default Intuned S3 settings.
    </ResponseField>

    <ResponseField name="input.fileNameOverride" type="string">
      Optional custom filename for the uploaded file. If not provided, uses the
      original filename or generates a unique name.
    </ResponseField>

    <ResponseField name="input.contentType" type="string">
      Optional MIME type for the uploaded file (e.g., “application/pdf”,
      “image/png”).
    </ResponseField>
  </Expandable>
</ResponseField>

## Returns: `Promise<Attachment>`

Promise that resolves to an [Attachment](../type-references/Attachment) object with file metadata and S3 utilities


# scrollToLoadContent
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/scrollToLoadContent



Automatically scrolls through infinite scroll content by repeatedly scrolling to the bottom
until no new content loads or maximum scroll limit is reached.

```typescript theme={null}
export declare function scrollToLoadContent(input: {
  source: Page | Locator;
  onScrollProgress?: CallableFunction;
  maxScrolls?: number;
  delayInMs?: number;
  minHeightChange?: number;
}): Promise<void>;
```

## Examples

<CodeGroup>
  ```typescript Basic Infinite Scroll handling theme={null}
  import { scrollToLoadContent } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    // Scroll through entire page content
    await page.goto("https://sandbox.intuned.dev/infinite-scroll");
    await scrollToLoadContent({
      source: page,
    });
    // Will keep scrolling until the page has loaded all content or the maxScrolls is reached.
  }
  ```

  ```typescript Scroll Specific Container theme={null}
  import { scrollToLoadContent } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    // Scroll through a specific scrollable div
    await page.goto(
      "https://docs.intunedhq.com/docs/00-getting-started/introduction"
    );
    // This will scroll the sidebar content only and watch its height to change.
    const container = page.locator("xpath=//div[@id='sidebar-content']");
    await scrollToLoadContent({
      source: container,
      maxScrolls: 20,
    });
    // Will keep scrolling until the sidebar content has loaded all content or the maxScrolls is reached.
  }
  ```
</CodeGroup>

## Arguments

<ResponseField name="input" type="Object">
  The input object containing the data to scroll to load content.

  <Expandable title="properties">
    <ResponseField name="input.source" type="Page | Locator">
      The Playwright Page or Locator to scroll.
    </ResponseField>

    <ResponseField name="input.onScrollProgress" type="Function">
      Optional callback function to call during each scroll iteration.
    </ResponseField>

    <ResponseField name="input.maxScrolls" type="number">
      Maximum number of scroll attempts before stopping. Defaults to 50.
    </ResponseField>

    <ResponseField name="input.delayInMs" type="number">
      Delay in milliseconds between scroll attempts. Defaults to 100.
    </ResponseField>

    <ResponseField name="input.minHeightChange" type="number">
      Minimum height change in pixels required to continue scrolling. Defaults to 100.
      If the page has loaded all content and we still haven't reached the maxScrolls, the minHeightChange will detect that no new content is loaded and stop the scrolling.
    </ResponseField>
  </Expandable>
</ResponseField>

## Returns: `Promise<void>`

Promise that resolves when scrolling is complete.


# uploadFileToS3
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/uploadFileToS3



Uploads files to AWS S3 storage with flexible configuration options.

This function accepts various file types including Playwright Download objects, binary data,
making it versatile for different upload scenarios. It automatically
handles file metadata and provides comprehensive S3 configuration options.

```typescript theme={null}
export declare function uploadFileToS3(input: {
  file: FileType;
  configs?: S3Configs;
  fileNameOverride?: string;
  contentType?: string;
}): Promise<Attachment>;
```

## S3 configuration fallback

The function uses a fallback system to determine S3 settings:

1. **S3Configs Parameter** - If provided, uses the explicit `configs` object with your custom settings.
2. **Environment Variables** - If no configs provided, automatically reads from environment variables:
   * `AWS_ACCESS_KEY_ID` - Your AWS access key
   * `AWS_SECRET_ACCESS_KEY` - Your AWS secret key
   * `AWS_REGION` - AWS region (e.g., "us-west-1")
   * `AWS_BUCKET` - S3 bucket name
   * `AWS_ENDPOINT_URL` - Optional custom S3 endpoint
   * Check [Environment Variables & Secrets](https://docs.intunedhq.com/docs/02-features/environment-variables-secrets) to learn more about setting environment variables.
3. **Intuned Defaults** - If environment variables aren't set, falls back to Intuned's managed S3 storage. See [S3 Attachment Storage](https://docs.intunedhq.com/docs/04-integrations/s3/s3-attachment-storage) for more details.

## Examples

<CodeGroup>
  ```typescript Upload Downloaded File theme={null}
  import { downloadFile, uploadFileToS3 } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    await page.goto("https://sandbox.intuned.dev/pdfs");
    const download = await downloadFile({
      page,
      trigger: page.locator("xpath=//tbody/tr[1]//*[name()='svg']"),
    });
    // Set your environment variables for the AWS credentials.
    // Check https://docs.intunedhq.com/docs/02-features/environment-variables-secrets to learn more about setting environment variables.
    const uploadedFile = await uploadFileToS3({
      file: download,
      configs: {
        bucket: process.env.AWS_BUCKET,
        region: process.env.AWS_REGION,
        accessKeyId: process.env.AWS_ACCESS_KEY_ID,
        secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
      },
      fileNameOverride: "reports/monthly-report.pdf",
    });

    console.log(`File uploaded: ${uploadedFile.suggestedFileName}`);
  }
  ```

  ```typescript Upload Binary Data theme={null}
  import { uploadFileToS3 } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    const fileBuffer = Buffer.from("Hello World", "utf8");
    const uploadedFile = await uploadFileToS3({
      file: fileBuffer,
      fileNameOverride: "data/text-file.txt",
      contentType: "text/plain",
    });

    // Generate a temporary download URL
    const downloadUrl = await uploadedFile.getSignedUrl();
    console.log(`Download URL: ${downloadUrl}`);
    return {
      downloadUrl: downloadUrl,
    };
  }
  ```
</CodeGroup>

## Arguments

<ResponseField name="input" type="Object">
  Configuration object for the upload operation

  <Expandable title="properties">
    <ResponseField name="input.file" type="FileType">
      The file to upload. Accepts [FileType](../type-references/FileType) types including Playwright Download objects, Uint8Array, Buffer, or ReadStream
    </ResponseField>

    <ResponseField name="input.configs" type="S3Configs">
      Optional [S3Configs](../type-references/S3Configs) for customizing the S3 upload. If not provided, uses environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`, `AWS_ENDPOINT_URL`, `AWS_BUCKET`). If environment variables aren't set, uses default Intuned S3 settings.
    </ResponseField>

    <ResponseField name="input.fileNameOverride" type="string">
      Optional custom filename for the uploaded file. If not provided, uses the original filename or generates a unique name.
    </ResponseField>

    <ResponseField name="input.contentType" type="string">
      Optional MIME type for the uploaded file (e.g., "application/pdf", "image/png")
    </ResponseField>
  </Expandable>
</ResponseField>

## Returns: `Promise<Attachment>`

Promise that resolves to an [Attachment](../type-references/Attachment) object with file metadata and utility methods


# validateDataUsingSchema
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/validateDataUsingSchema



Validates data against a JSON schema using the AJV validator.
This function can validate any given data against a given schema. It supports validating custom data types such as the [Attachment](../type-references/Attachment) type.

```typescript theme={null}
export declare function validateDataUsingSchema(input: {
  data: Record<string, any>[] | Record<string, any>;
  schema: Record<string, any>;
}): void;
```

## Examples

<CodeGroup>
  ```typescript Basic Validation theme={null}
  import { validateDataUsingSchema } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    const uploadData = {
      file: {
        fileName: "documents/report.pdf",
        bucket: "my-bucket",
        region: "us-east-1",
        key: "documents/report.pdf",
        endpoint: null,
        suggestedFileName: "Monthly Report.pdf",
        fileType: "document",
      },
      name: "Test File Upload",
    };

    const uploadSchema = {
      type: "object",
      required: ["file", "name"],
      properties: {
        file: { type: "attachment" },
        name: { type: "string" },
      },
    };

    validateDataUsingSchema({ data: uploadData, schema: uploadSchema });
    // Validation passes with Attachment type
    console.log("Validation passed");
    return "Validation passed";
  }
  ```

  ```typescript Invalid Data Validation theme={null}
  import { validateDataUsingSchema } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    const userData = {
      name: "John Doe",
      email: "john@example.com",
    };

    const userSchema = {
      type: "object",
      required: ["name", "email", "age"],
      properties: {
        name: { type: "string", minLength: 1 },
        email: { type: "string", format: "email" },
        age: { type: "number", minimum: 0 },
      },
    };

    validateDataUsingSchema({ data: userData, schema: userSchema }); // this will throw
    // Validation fails, throws ValidationError
    console.log("Never reached");
    return "Never reached";
  }
  ```
</CodeGroup>

## Arguments

<ResponseField name="input" type="Object">
  The input object containing data and schema

  <Expandable title="properties">
    <ResponseField name="input.data" type="Record<string, any> | Array<Record<string, any>>">
      The data to validate. Can be a single data object or an array of data objects
    </ResponseField>

    <ResponseField name="input.schema" type="Record<string, any>">
      JSON schema object defining validation rules
    </ResponseField>
  </Expandable>
</ResponseField>

## Returns: `void`

Returns nothing if validation passes, throws ValidationError if it fails

## Raises

### ValidationError

If validation fails


# waitForDomSettled
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/waitForDomSettled



Waits for DOM mutations to settle without executing any function first. This helper uses a MutationObserver to monitor DOM changes and waits until the DOM has been stable (no mutations) for the specified settle duration.

```typescript theme={null}
export declare function waitForDomSettled(options: {
  source: Page | Locator;
  settleDurationMs?: number;
  timeoutInMs?: number;
}): Promise<boolean>;
```

## `waitForDomSettled` vs `withNetworkSettledWait`

* Use `waitForDomSettled` when watching **DOM mutations** (elements added/removed/modified, loading spinners, dynamic content injection)
* Use [withNetworkSettledWait](../functions/withNetworkSettledWait) when watching **network requests** (API calls, form submissions, resource loading)

## Examples

<CodeGroup>
  ```typescript Wait After Navigation theme={null}
  import { waitForDomSettled } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    // Navigate to page with dynamic content
    await page.goto("https://sandbox.intuned.dev/lists/table");

    // Wait for all DOM mutations to complete
    const settled = await waitForDomSettled({
      source: page,
      settleDurationMs: 1000,
      timeoutInMs: 20000,
    });

    if (settled) {
      // DOM is stable, safe to extract data
      const rows = await page.locator("table tr").all();
      return rows.length;
    }
    return 0;
  }
  ```

  ```typescript Watch Specific Container theme={null}
  import { waitForDomSettled } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    await page.goto("https://sandbox.intuned.dev/lists/table");

    // Only watch table container (ignore header/footer changes)
    const table = page.locator("table");
    const settled = await waitForDomSettled({
      source: table,
      settleDurationMs: 800,
      timeoutInMs: 15000,
    });

    if (settled) {
      // Table has finished loading
      const rows = await table.locator("tr").count();
      return rows;
    }
    return 0;
  }
  ```
</CodeGroup>

## Arguments

<ResponseField name="options" type="Object">
  Configuration object for DOM settlement monitoring

  <Expandable title="properties">
    <ResponseField name="options.source" type="Page | Locator">
      The Playwright Page or Locator to monitor for DOM changes. When a Page is provided, monitors the entire document. When a Locator is provided, monitors only that specific element.
    </ResponseField>

    <ResponseField name="options.settleDurationMs" type="number">
      Milliseconds the DOM must remain unchanged to be considered settled. Defaults to 500
    </ResponseField>

    <ResponseField name="options.timeoutInMs" type="number">
      Maximum milliseconds to wait before giving up. Defaults to 30000
    </ResponseField>
  </Expandable>
</ResponseField>

## Returns: `Promise<boolean>`

Promise that resolves to true if DOM settled within timeout, false if timeout or error occurred


# withNetworkSettledWait
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/withNetworkSettledWait



Executes a callback function and waits for network activity to settle before returning.

```typescript theme={null}
export declare function withNetworkSettledWait<T>(callback: (page: Page) => Promise<T>,
  options?: {
    page: Page;
    timeoutInMs?: number;
    maxInflightRequests?: number;
  }): Promise<T>;
```

## `withNetworkSettledWait` vs `waitForDomSettled`

* Use `withNetworkSettledWait` when watching **network requests** (API calls, form submissions, resource loading)
* Use [waitForDomSettled](../functions/waitForDomSettled) when watching **DOM mutations** (elements added/removed/modified, loading spinners, dynamic content injection)

## Examples

<CodeGroup>
  ```typescript Wrapper with Inline Function theme={null}
  import { withNetworkSettledWait } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    await page.goto("https://sandbox.intuned.dev/infinite-scroll");

    // Execute action and wait for network to settle
    const result = await withNetworkSettledWait(
      async (page) => {
        // scroll to load more content
        await page.evaluate(() => {
          window.scrollTo(0, document.body.scrollHeight);
        });
        return "scrolled";
      },
      {
        page,
        timeoutInMs: 15000,
        maxInflightRequests: 0,
      }
    );
    console.log(result); // "scrolled"
    return result;
  }
  ```

  ```typescript Click Link with Network Wait theme={null}
  import { withNetworkSettledWait } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    await page.goto("https://sandbox.intuned.dev/");

    // Click Object Extractors link and wait for page to load
    await withNetworkSettledWait(
      async (page) => {
        await page.getByText("Object Extractors").click();
      },
      {
        page,
        timeoutInMs: 10000,
        maxInflightRequests: 0,
      }
    );
    // Object Extractors page loaded
    const title = await page.locator("div h2").innerText();
    return title;
  }
  ```
</CodeGroup>

## Arguments

<ResponseField name="callback" type="Function">
  The callback function to execute. Receives the page object as parameter
</ResponseField>

<ResponseField name="options" type="Object">
  Configuration options for network monitoring

  <Expandable title="properties">
    <ResponseField name="options.page" type="Page">
      The Playwright page object to monitor for network activity
    </ResponseField>

    <ResponseField name="options.timeoutInMs" type="number">
      Maximum time to wait in milliseconds before timing out. Defaults to 30000
    </ResponseField>

    <ResponseField name="options.maxInflightRequests" type="number">
      Maximum number of pending requests to consider as "settled". Defaults to 0 (waits for all requests to complete)
    </ResponseField>
  </Expandable>
</ResponseField>

## Returns: `Promise<T>`

Promise that resolves to the callback's return value after network settles


# Attachment
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/helpers/type-references/Attachment



Represents an uploaded file stored in AWS S3 with metadata and utility methods.
Provides a structured way to handle file information for files stored in S3, including methods for generating presigned URLs, serialization, and accessing file metadata.

```typescript theme={null}
export interface Attachment {
  fileName: string;
  key: string;
  bucket: string;
  region: string;
  endpoint?: string | null;
  suggestedFileName: string;
  fileType?: AttachmentType | null;
  toJSON(): Record<string, string>;
  toDict(): Record<string, string>;
  getS3Key(): string;
  getFilePath(): string;
  getSignedUrl(expiration?: number): Promise<string>;
}
```

## Properties

<ParamField type="string">
  The name/key of the file in the S3 bucket
</ParamField>

<ParamField type="string">
  The S3 object key/path
</ParamField>

<ParamField type="string">
  The S3 bucket name where the file is stored
</ParamField>

<ParamField type="string">
  The AWS region where the S3 bucket is located
</ParamField>

<ParamField type="string">
  Optional custom S3 endpoint URL. Defaults to undefined for standard AWS S3
</ParamField>

<ParamField type="string">
  A human-readable filename suggestion for downloads or display
</ParamField>

<ParamField type="AttachmentType">
  The file type of the file
</ParamField>

## Methods

<AccordionGroup>
  <Accordion title="toJSON">
    Returns a JSON-serializable record representation of the file.

    **Returns:** `Record<string, string>`

    Complete model data including all fields
  </Accordion>

  <Accordion title="toDict">
    Converts the file metadata to a record.

    **Returns:** `Record<string, string>`

    Record with fileName, key, bucket, region, endpoint, suggestedFileName, and fileType
  </Accordion>

  <Accordion title="getS3Key">
    Returns the full S3 URL for the file.

    **Returns:** `string`

    Complete S3 URL in format: [https://bucket.s3.region.amazonaws.com/filename](https://bucket.s3.region.amazonaws.com/filename)
  </Accordion>

  <Accordion title="getFilePath">
    Returns the file path/key within the S3 bucket.

    **Returns:** `string`

    The fileName property (S3 object key)
  </Accordion>

  <Accordion title="getSignedUrl">
    Generates a presigned URL for secure, temporary access to the file.

    <ParamField type="any">
      URL expiration time in seconds. Defaults to 432000 (5 days)
    </ParamField>

    **Returns:** `Promise<string>`

    Presigned URL for downloading the file
  </Accordion>
</AccordionGroup>

## Examples

<CodeGroup>
  ```typescript Basic Usage theme={null}
  import { uploadFileToS3, Attachment } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";
  interface Params {}
  export default async function handler(params: Params, page: Page, context: BrowserContext){
    const uploadedFile: Attachment = await uploadFileToS3({
      file: myFile,
      configs: s3Config
    });
    // Access file properties
    console.log(uploadedFile.fileName);
    console.log(uploadedFile.suggestedFileName);
  }
  ```

  ```typescript Working with Presigned URLs theme={null}
  import { uploadFileToS3, Attachment } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";
  interface Params {}
  export default async function handler(params: Params, page: Page, context: BrowserContext){
    const uploadedFile: Attachment = await uploadFileToS3({
      file: myFile
    });
    // Generate a presigned URL for temporary access
    const downloadUrl = await uploadedFile.getSignedUrl(3600); // 1 hour expiration
    // Get the permanent S3 URL
    const s3Url = uploadedFile.getS3Key();
  }
  ```

  ```typescript Serialization theme={null}
  import { uploadFileToS3, Attachment } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";
  interface Params {}
  export default async function handler(params: Params, page: Page, context: BrowserContext){
    const uploadedFile: Attachment = await uploadFileToS3({
      file: myFile
    });
    // Convert to dictionary for storage or API responses
    const fileDict = uploadedFile.toDict();
    const fileJson = uploadedFile.toJSON();
  }
  ```
</CodeGroup>


# AttachmentType
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/helpers/type-references/AttachmentType



Union type representing the supported attachment file types.

Currently supported types:

* `"document"`: Document files (PDFs, Word docs, etc.)

```typescript theme={null}
export type AttachmentType = "document";
```


# FileType
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/helpers/type-references/FileType



A union type representing different file formats that can be processed by the SDK.

Provides flexibility in how files can be passed to various functions, supporting multiple input formats.

```typescript theme={null}
export type FileType = Download | Uint8Array | Buffer | ReadStream;
```

## Examples

<CodeGroup>
  ```typescript Using Download Object theme={null}
  import { downloadFile, uploadFileToS3 } from "@intuned/browser";
  export default async function handler(params, page, context) {
    // From a browser download
    const download = await downloadFile({
      page,
      trigger: "https://example.com/file.pdf",
    });
    const uploaded = await uploadFileToS3({
      file: download,
      configs: s3Config,
    });
  }
  ```

  ```typescript Using Buffer theme={null}
  import { uploadFileToS3 } from "@intuned/browser";
  export default async function handler(params, page, context) {
    // From raw buffer
    const fileBuffer = Buffer.from("PDF content here...", "utf8");
    const uploaded = await uploadFileToS3({
      file: fileBuffer,
      fileNameOverride: "generated.pdf",
      configs: s3Config,
    });
  }
  ```

  ```typescript Using ReadStream theme={null}
  import { uploadFileToS3 } from "@intuned/browser";
  import { createReadStream } from "fs";
  export default async function handler(params, page, context) {
    // From a file stream
    const fileStream = createReadStream("/path/to/document.pdf");
    const uploaded = await uploadFileToS3({
      file: fileStream,
      fileNameOverride: "renamed.pdf",
      configs: s3Config,
    });
  }
  ```
</CodeGroup>


# S3Configs
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/helpers/type-references/S3Configs



Configuration for AWS S3 storage operations.
Defines the parameters needed to connect to and interact with AWS S3 storage services. Supports both standard AWS S3 and S3-compatible storage services through custom endpoints.

```typescript theme={null}
export interface S3Configs {
  bucket?: string;
  region?: string;
  accessKeyId?: string;
  secretAccessKey?: string;
  endpoint?: string;
}
```

## Properties

<ParamField type="string">
  Name of the S3 bucket to store files in. Must be a valid S3 bucket name following AWS naming conventions.
</ParamField>

<ParamField type="string">
  AWS region where the S3 bucket is located. Examples: "us-east-1", "eu-west-1", "ap-southeast-1"
</ParamField>

<ParamField type="string">
  AWS access key ID for authentication. If undefined, will attempt to use default AWS credentials or environment variables.
</ParamField>

<ParamField type="string">
  AWS secret access key for authentication. If undefined, will attempt to use default AWS credentials or environment variables.
</ParamField>

<ParamField type="string">
  Custom endpoint URL for S3-compatible storage services. Use this for services like MinIO, DigitalOcean Spaces, or other S3-compatible APIs. For standard AWS S3, leave this undefined.
</ParamField>

## Examples

<CodeGroup>
  ```typescript Basic Configuration theme={null}
  import { uploadFileToS3, S3Configs } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";
  interface Params {}
  export default async function handler(params: Params, page: Page, context: BrowserContext){
    // Using explicit credentials
    const s3Config: S3Configs = {
      accessKeyId: "accessKeyId",
      secretAccessKey: "SecretAccessKeyId",
      bucket: "my-app-uploads",
      region: "us-east-1"
    };
  }
  ```

  ```typescript Environment Variables Configuration theme={null}
  import { uploadFileToS3, S3Configs } from "@intuned/browser";
  export default async function handler(params, page, context){
    // Credentials will be picked up from environment or IAM roles
    const s3Config: S3Configs = {
      bucket: "my-app-uploads",
      region: "us-west-2"
    };
  }
  ```
</CodeGroup>


# Trigger
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/helpers/type-references/Trigger



A union type representing different methods to trigger file downloads in web automation.

This type standardizes how download operations can be initiated, providing multiple approaches for different automation scenarios.

**Type variants:**

* `string`: Direct URL string to download from
* `Locator`: Playwright Locator object pointing to a clickable download element
* `(page: Page) => Promise<void>`: Custom async function that takes a Page and triggers the download

```typescript theme={null}
export type Trigger = string | Locator | ((page: Page) => Promise<void>);
```

## Examples

<CodeGroup>
  ```typescript URL String Trigger theme={null}
  import { downloadFile } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    // Direct download from URL
    const download = await downloadFile({
      page,
      trigger: "https://example.com/report.pdf",
    });
  }
  ```

  ```typescript Locator Trigger theme={null}
  import { downloadFile } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    // Click a download button
    const download = await downloadFile({
      page,
      trigger: page.locator("#download-btn"),
    });
  }
  ```

  ```typescript Callback Trigger theme={null}
  import { downloadFile } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    // Custom download logic
    const download = await downloadFile({
      page,
      trigger: async (p) => {
        await p.locator("button.prepare").click();
        await p.waitForSelector(".ready");
        await p.locator("a.download-link").click();
      },
    });
  }
  ```
</CodeGroup>


# extractArrayFromLocator
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/optimized-extractors/functions/extractArrayFromLocator



<Warning> **Deprecated:** This function is deprecated and will be removed in the future. </Warning>

Extracts an array of structured data from a locator.

```typescript theme={null}
export declare function extractArrayFromLocator(
  locator: Locator,
  options: {
    label: string;
    itemEntityName: string;
    itemEntitySchema: SimpleArrayItemSchema;
    strategy?: ImageStrategy | HtmlStrategy;
    prompt?: string;
    optionalPropertiesInvalidator?: (
      result: Record<string, string>[]
    ) => string[];
    variantKey?: string;
    apiKey?: string;
  }
): Promise<Record<string, string>[]>;
```

## Examples

<CodeGroup>
  ```typescript extractArrayFromLocator theme={null}
  import { extractArrayFromLocator } from "@intuned/browser/optimized-extractors";

  await page.goto("https://books.toscrape.com/");
  const books = await extractArrayFromLocator(page.locator("section"), {
    itemEntityName: "book",
    label: "books-extraction",
    itemEntitySchema: {
      type: "object",
      required: ["name"],
      properties: {
        name: {
          type: "string",
          description: "book name",
          primary: true,
        },
      },
    },
  });

  console.log(books);

  // output:
  // [
  // ...
  // { name: 'Olio' },
  // { name: 'Mesaerion: The Best Science Fiction Stories 1800-1849' },
  // { name: 'Libertarianism for Beginners' },
  // { name: "It's Only the Himalayas" }
  // ...
  // ]
  ```
</CodeGroup>

## Arguments

<ResponseField name="locator" type="any">
  The Playwright Locator object from which to extract the data.
</ResponseField>

<ResponseField name="options" type="object">
  <Expandable title="properties">
    <ResponseField name="options.label" type="any">
      A label for this extraction process, used for billing and monitoring.
    </ResponseField>

    <ResponseField name="options.itemEntityName" type="any">
      The name of the entity items being extracted. Must be 1–50 characters long and can only contain letters, digits, periods, underscores, and hyphens.
    </ResponseField>

    <ResponseField name="options.itemEntitySchema" type="any">
      The schema of the entity items being extracted.
    </ResponseField>

    <ResponseField name="options.strategy" type="any">
      Optional. The strategy to use for extraction, if not provided, the html strategy with claude haiku will be used.
    </ResponseField>

    <ResponseField name="options.prompt" type="any">
      Optional. A prompt to guide the extraction process.
    </ResponseField>

    <ResponseField name="options.optionalPropertiesInvalidator" type="any">
      Optional. A function to invalidate optional properties.
    </ResponseField>

    <ResponseField name="options.variantKey" type="any">
      Optional. A variant key for the extraction process.
    </ResponseField>

    <ResponseField name="options.apiKey" type="any">
      Optional. An API key for AI extraction. Extractions made with your API key won't be billed to your account.
    </ResponseField>
  </Expandable>
</ResponseField>

## Returns: `any`

A promise that resolves to a list of extracted data.


# extractArrayFromPage
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/optimized-extractors/functions/extractArrayFromPage



<Warning> **Deprecated:** This function is deprecated and will be removed in the future. </Warning>

Extracts an array of structured data from a web page in an optimized way. This function uses AI for the first few extractions until it collects multiple examples, then builds reliable selectors in the background for improved efficiency.

```typescript theme={null}
export declare function extractArrayFromPage(
  page: Page,
  options: {
    label: string;
    itemEntityName: string;
    itemEntitySchema: SimpleArrayItemSchema;
    strategy?: ImageStrategy | HtmlStrategy;
    prompt?: string;
    optionalPropertiesInvalidator?: (
      result: Record<string, string>[]
    ) => string[];
    variantKey?: string;
    apiKey?: string;
  }
): Promise<Record<string, string>[]>;
```

## Examples

<CodeGroup>
  ```typescript extractArrayFromPage theme={null}
  import { extractArrayFromPage } from "@intuned/browser/optimized-extractors";

  await page.goto("https://books.toscrape.com/");
  const books = await extractArrayFromPage(page, {
    strategy: {
      model: "gpt4-turbo",
      type: "HTML",
    },
    itemEntityName: "book",
    label: "books-extraction",
    itemEntitySchema: {
      type: "object",
      required: ["name"],
      properties: {
        name: {
          type: "string",
          description: "book name",
          primary: true,
        },
      },
    },
  });

  console.log(books);

  // output:
  // [
  // ...
  // { name: 'Olio' },
  // { name: 'Mesaerion: The Best Science Fiction Stories 1800-1849' },
  // { name: 'Libertarianism for Beginners' },
  // { name: "It's Only the Himalayas" }
  // ...
  // ]
  ```
</CodeGroup>

## Arguments

<ResponseField name="page" type="any">
  The Playwright Page object from which to extract the data.
</ResponseField>

<ResponseField name="options" type="object">
  <Expandable title="properties">
    <ResponseField name="options.label" type="any">
      A label for this extraction process, used for billing and monitoring.
    </ResponseField>

    <ResponseField name="options.itemEntityName" type="any">
      The name of the entity items being extracted. Must be 1–50 characters long and can only contain letters, digits, periods, underscores, and hyphens.
    </ResponseField>

    <ResponseField name="options.itemEntitySchema" type="any">
      The schema of the entity items being extracted.
    </ResponseField>

    <ResponseField name="options.strategy" type="any">
      Optional. The strategy to use for extraction, if not provided, the html strategy with claude haiku will be used.
    </ResponseField>

    <ResponseField name="options.prompt" type="any">
      Optional. A prompt to guide the extraction process.
    </ResponseField>

    <ResponseField name="options.optionalPropertiesInvalidator" type="any">
      Optional. A function to invalidate optional properties.
    </ResponseField>

    <ResponseField name="options.variantKey" type="any">
      Optional. A variant key for the extraction process, use this when the page has multiple variants/shapes.
    </ResponseField>

    <ResponseField name="options.apiKey" type="any">
      Optional. An API key for AI extraction. Extractions made with your API key won't be billed to your account.
    </ResponseField>
  </Expandable>
</ResponseField>

## Returns: `any`

A promise that resolves to a list of extracted data.


# extractObjectFromLocator
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/optimized-extractors/functions/extractObjectFromLocator



<Warning> **Deprecated:** This function is deprecated and will be removed in the future. </Warning>

Extracts a structured object from a locator.

```typescript theme={null}
export declare function extractObjectFromLocator(
  locator: Locator,
  options: {
    label: string;
    entityName: string;
    entitySchema: SimpleObjectSchema;
    strategy?: ImageStrategy | HtmlStrategy;
    prompt?: string;
    optionalPropertiesInvalidator?: (
      result: Record<string, string | null> | null
    ) => string[];
    variantKey?: string;
    apiKey?: string;
  }
): Promise<Record<string, string | null> | null>;
```

## Examples

<CodeGroup>
  ```typescript extractObjectFromLocator theme={null}
  import { extractObjectFromLocator } from "@intuned/browser/optimized-extractors";

  await page.goto(
    "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"
  );
  const book = await extractObjectFromLocator(page.locator(".page_inner"), {
    entityName: "book",
    label: "book-extraction",
    entitySchema: {
      type: "object",
      required: ["name", "price", "reviews"],
      properties: {
        name: {
          type: "string",
          description: "book name",
        },
        price: {
          type: "string",
          description: "book price",
        },
        reviews: {
          type: "string",
          description: "Number of reviews",
        },
      },
    },
  });

  console.log(book);

  // output:
  // { name: 'A Light in the Attic', price: '£51.77', reviews: '0' }
  ```
</CodeGroup>

## Arguments

<ResponseField name="locator" type="any">
  The Playwright Locator object from which to extract the data.
</ResponseField>

<ResponseField name="options" type="object">
  <Expandable title="properties">
    <ResponseField name="options.label" type="any">
      A label for this extraction process, used for billing and monitoring.
    </ResponseField>

    <ResponseField name="options.entityName" type="any">
      The name of the entity being extracted. Must be 1–50 characters long and can only contain letters, digits, periods, underscores, and hyphens.
    </ResponseField>

    <ResponseField name="options.entitySchema" type="any">
      The schema of the entity being extracted.
    </ResponseField>

    <ResponseField name="options.strategy" type="any">
      Optional. The strategy to use for extraction, if not provided, the html strategy with claude haiku will be used.
    </ResponseField>

    <ResponseField name="options.prompt" type="any">
      Optional. A prompt to guide the extraction process.
    </ResponseField>

    <ResponseField name="options.optionalPropertiesInvalidator" type="any">
      Optional. A function to invalidate optional properties.
    </ResponseField>

    <ResponseField name="options.variantKey" type="any">
      Optional. A variant key for the extraction process.
    </ResponseField>

    <ResponseField name="options.apiKey" type="any">
      Optional. An API key for AI extraction. Extractions made with your API key won't be billed to your account.
    </ResponseField>
  </Expandable>
</ResponseField>

## Returns: `any`

A promise that resolves to the extracted object.


# extractObjectFromPage
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/optimized-extractors/functions/extractObjectFromPage



<Warning> **Deprecated:** This function is deprecated and will be removed in the future. </Warning>

Extracts a structured object from a web page.

```typescript theme={null}
export declare function extractObjectFromPage(
  page: Page,
  options: {
    label: string;
    entityName: string;
    entitySchema: SimpleObjectSchema;
    strategy?: ImageStrategy | HtmlStrategy;
    prompt?: string;
    optionalPropertiesInvalidator?: (
      result: Record<string, string | null> | null
    ) => string[];
    variantKey?: string;
    apiKey?: string;
  }
): Promise<Record<string, string | null> | null>;
```

## Examples

<CodeGroup>
  ```typescript extractObjectFromPage theme={null}
  import { extractObjectFromPage } from "@intuned/browser/optimized-extractors";

  await page.goto(
    "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"
  );
  const book = await extractObjectFromPage(page, {
    entityName: "book",
    label: "book-extraction",
    entitySchema: {
      type: "object",
      required: ["name", "price", "reviews"],
      properties: {
        name: {
          type: "string",
          description: "book name",
        },
        price: {
          type: "string",
          description: "book price",
        },
        reviews: {
          type: "string",
          description: "Number of reviews",
        },
      },
    },
  });

  console.log(book);

  // output:
  // { name: 'A Light in the Attic', price: '£51.77', reviews: '0' }
  ```
</CodeGroup>

## Arguments

<ResponseField name="page" type="any">
  The Playwright Page object from which to extract the data.
</ResponseField>

<ResponseField name="options" type="object">
  <Expandable title="properties">
    <ResponseField name="options.label" type="any">
      A label for this extraction process, used for billing and monitoring.
    </ResponseField>

    <ResponseField name="options.entityName" type="any">
      The name of the entity being extracted. Must be 1–50 characters long and can only contain letters, digits, periods, underscores, and hyphens.
    </ResponseField>

    <ResponseField name="options.entitySchema" type="any">
      The schema of the entity being extracted.
    </ResponseField>

    <ResponseField name="options.strategy" type="any">
      Optional. The strategy to use for extraction, if not provided, the html strategy with claude haiku will be used.
    </ResponseField>

    <ResponseField name="options.prompt" type="any">
      Optional. A prompt to guide the extraction process.
    </ResponseField>

    <ResponseField name="options.optionalPropertiesInvalidator" type="any">
      Optional. A function to invalidate optional properties.
    </ResponseField>

    <ResponseField name="options.variantKey" type="any">
      Optional. A variant key for the extraction process.
    </ResponseField>

    <ResponseField name="options.apiKey" type="any">
      Optional. An API key for AI extraction. Extractions made with your API key won't be billed to your account.
    </ResponseField>
  </Expandable>
</ResponseField>

## Returns: `any`

A promise that resolves to the extracted object.


# SimpleArrayItemSchema
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/optimized-extractors/type-references/SimpleArrayItemSchema



A simple array item schema with properties.

```typescript theme={null}
export interface SimpleArrayItemSchema extends BasicSchema {
  type: "object";
  properties: Record<string, SimpleArrayStringSchema>;
  required: string[];
}
```

## Properties

<ParamField type="'object'" />

<ParamField type="Record<string, SimpleArrayStringSchema>" />

<ParamField type="string[]" />


# SimpleArrayStringSchema
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/optimized-extractors/type-references/SimpleArrayStringSchema



A simple array schema with string properties.

```typescript theme={null}
export interface SimpleArrayStringSchema extends BasicSchema {
  type: "string";
  primary?: boolean;
}
```

## Properties

<ParamField type="'string'" />

<ParamField type="boolean" />


# SimpleObjectSchema
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/optimized-extractors/type-references/SimpleObjectSchema



A simple object schema with properties.

```typescript theme={null}
export interface SimpleObjectSchema extends BasicSchema {
  type: "object";
  properties: Record<string, SimpleObjectStringSchema>;
  required: string[];
}
```

## Properties

<ParamField type="'object'" />

<ParamField type="Record<string, SimpleObjectStringSchema>" />

<ParamField type="string[]" />


# SimpleObjectStringSchema
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/optimized-extractors/type-references/SimpleObjectStringSchema



A simple object schema with string properties.

```typescript theme={null}
export interface SimpleObjectStringSchema extends BasicSchema {
  type: "string";
}
```

## Properties

<ParamField type="'string'" />


# TypeScript SDK
Source: https://intunedhq.com/docs/automation-sdks/intuned-sdk/typescript/overview



Browser automation helpers for TypeScript/JavaScript, built on [Playwright](https://playwright.dev/). This package provides utilities for common automation tasks—AI-powered data extraction, navigation with retries, pagination handling, and more.

## Installation

```bash theme={null}
npm install @intuned/browser
```

<Note>
  When using [Intuned](https://app.intuned.io), this package is pre-installed in
  every TypeScript project.
</Note>

## Quick example

```typescript theme={null}
import { Page, BrowserContext } from "playwright";
import { extractStructuredData, isPageLoaded } from "@intuned/browser/ai";
import { goToUrl } from "@intuned/browser";

interface Params {}

export default async function automation(
  params: Params,
  page: Page,
  context: BrowserContext
) {
  await goToUrl({ page, url: "https://books.toscrape.com" });

  const loaded = await isPageLoaded({ page });
  if (!loaded) {
    throw new Error("Page is not loaded, cannot extract data");
  }

  // Extract all book listings from the page
  const books = await extractStructuredData({
    source: page,
    dataSchema: {
      type: "object",
      properties: {
        products: {
          type: "array",
          items: {
            type: "object",
            properties: {
              title: { type: "string" },
              price: { type: "string" },
            },
          },
        },
      },
    },
    prompt: "Extract all book listings with their titles and prices",
    strategy: "HTML",
    model: "claude-haiku-4-5-20251001",
  });

  return books;
}
```

## AI module

AI-powered utilities for data extraction and page analysis. These functions use AI and incur costs.

| Function                                                        | Description                                                                    |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| [`extractStructuredData`](./ai/functions/extractStructuredData) | Extract structured data from pages using AI with JSON Schema or Zod validation |
| [`isPageLoaded`](./ai/functions/isPageLoaded)                   | Detect when a page has finished loading                                        |

<Tip>AI functions support caching and matching to reduce costs.</Tip>

## Helpers module

| Function                                                                 | Description                                        |
| ------------------------------------------------------------------------ | -------------------------------------------------- |
| [`goToUrl`](./helpers/functions/goToUrl)                                 | Navigate with automatic retries and error handling |
| [`withNetworkSettledWait`](./helpers/functions/withNetworkSettledWait)   | Wait for network requests to complete              |
| [`waitForDomSettled`](./helpers/functions/waitForDomSettled)             | Wait for DOM mutations to finish                   |
| [`scrollToLoadContent`](./helpers/functions/scrollToLoadContent)         | Load infinite-scroll content                       |
| [`clickUntilExhausted`](./helpers/functions/clickUntilExhausted)         | Click "Load More" buttons until all content loads  |
| [`extractMarkdown`](./helpers/functions/extractMarkdown)                 | Convert pages to markdown                          |
| [`downloadFile`](./helpers/functions/downloadFile)                       | Download files with different triggers             |
| [`saveFileToS3`](./helpers/functions/saveFileToS3)                       | Download and upload files to S3                    |
| [`uploadFileToS3`](./helpers/functions/uploadFileToS3)                   | Upload files with custom S3 configurations         |
| [`filterEmptyValues`](./helpers/functions/filterEmptyValues)             | Remove empty values from data                      |
| [`validateDataUsingSchema`](./helpers/functions/validateDataUsingSchema) | Validate data against schemas                      |
| [`processDate`](./helpers/functions/processDate)                         | Parse and normalize dates                          |
| [`sanitizeHtml`](./helpers/functions/sanitizeHtml)                       | Clean and sanitize HTML                            |
| [`resolveUrl`](./helpers/functions/resolveUrl)                           | Resolve relative URLs to absolute paths            |

## Requirements

* Node.js 18+
* Playwright (`npm install playwright && npx playwright install`)
* For AI functions: API key for your AI provider (set via environment variable or function parameter)


# Overview
Source: https://intunedhq.com/docs/automation-sdks/overview



## Intuned Browser SDK

Browser automation helpers built on [Playwright](https://playwright.dev/). These SDKs extend Playwright with utilities for common automation tasks—AI-powered data extraction, pagination handling, file operations, and more.

Use these helpers in any Playwright project, or with the [Intuned platform](https://app.intuned.io) for managed infrastructure and deployment.

<CardGroup>
  <Card title="TypeScript/JavaScript" icon="js" href="./intuned-sdk/typescript">
    `@intuned/browser` - Browser automation helpers for Node.js
  </Card>

  <Card title="Python" icon="python" href="./intuned-sdk/python">
    `intuned_browser` - Browser automation helpers for Python
  </Card>
</CardGroup>

## What's included

**AI helpers** — Extract structured data from pages using vision models. Detect page load states intelligently. These functions use AI and incur costs.

**Automation helpers** — Navigate with retries, wait for network/DOM to settle, handle infinite scroll and "Load More" pagination, convert pages to markdown.

**File utilities** — Download files from pages, upload to S3, handle file operations.

**Data processing** — Validate against schemas, sanitize HTML, parse dates, filter empty values.

## Requirements

Both SDKs require:

* Playwright installed and configured
* For AI functions: an API key for the AI provider (OpenAI, Anthropic, etc.)

## Using with Intuned

When deployed to [Intuned](https://app.intuned.io), these SDKs are pre-installed and configured. AI functions use Intuned's built-in AI integration, and file utilities can leverage Intuned's storage.

For standalone use, install the SDK directly and configure AI providers as needed.


# Delete AuthSession
Source: https://intunedhq.com/docs/client-apis/api-reference/projectsauthsessions/delete-authsession

delete /{workspaceId}/projects/{projectName}/auth-sessions/{authSessionId}
Delete an AuthSession by ID.



# Get AuthSession
Source: https://intunedhq.com/docs/client-apis/api-reference/projectsauthsessions/get-authsession

get /{workspaceId}/projects/{projectName}/auth-sessions/{authSessionId}
Get an AuthSession by ID.



# Get AuthSessions
Source: https://intunedhq.com/docs/client-apis/api-reference/projectsauthsessions/get-authsessions

get /{workspaceId}/projects/{projectName}/auth-sessions
Get all AuthSessions in a Project.



# Create AuthSession - Result
Source: https://intunedhq.com/docs/client-apis/api-reference/projectsauthsessionscreate/create-authsession--result

get /{workspaceId}/projects/{projectName}/auth-sessions/create/{operationId}/result
Get AuthSession creation result.



# Create AuthSession - Start
Source: https://intunedhq.com/docs/client-apis/api-reference/projectsauthsessionscreate/create-authsession--start

post /{workspaceId}/projects/{projectName}/auth-sessions/create
Start creating an AuthSession.



# Update AuthSession - Result
Source: https://intunedhq.com/docs/client-apis/api-reference/projectsauthsessionsupdate/update-authsession--result

get /{workspaceId}/projects/{projectName}/auth-sessions/{authSessionId}/update/{operationId}/result
Get AuthSession update result.



# Update AuthSession - Start
Source: https://intunedhq.com/docs/client-apis/api-reference/projectsauthsessionsupdate/update-authsession--start

post /{workspaceId}/projects/{projectName}/auth-sessions/{authSessionId}/update
Start updating an AuthSession.



# Validate AuthSession - Result
Source: https://intunedhq.com/docs/client-apis/api-reference/projectsauthsessionsvalidate/validate-authsession--result

get /{workspaceId}/projects/{projectName}/auth-sessions/{authSessionId}/validate/{operationId}/result
Get AuthSession validation result.



# Validate AuthSession - Start
Source: https://intunedhq.com/docs/client-apis/api-reference/projectsauthsessionsvalidate/validate-authsession--start

post /{workspaceId}/projects/{projectName}/auth-sessions/{authSessionId}/validate/start
Start AuthSession validation.



# Create Job
Source: https://intunedhq.com/docs/client-apis/api-reference/projectsjobs/create-job

post /{workspaceId}/projects/{projectName}/jobs
Create a new Job for a Project.



# Delete Job
Source: https://intunedhq.com/docs/client-apis/api-reference/projectsjobs/delete-job

delete /{workspaceId}/projects/{projectName}/jobs/{jobId}
Delete a Job by ID.



# Get Job
Source: https://intunedhq.com/docs/client-apis/api-reference/projectsjobs/get-job

get /{workspaceId}/projects/{projectName}/jobs/{jobId}
Get a Job by ID.



# Get Jobs
Source: https://intunedhq.com/docs/client-apis/api-reference/projectsjobs/get-jobs

get /{workspaceId}/projects/{projectName}/jobs
Get all Jobs in a Project.



# Pause Job
Source: https://intunedhq.com/docs/client-apis/api-reference/projectsjobs/pause-job

post /{workspaceId}/projects/{projectName}/jobs/{jobId}/pause
Pause a Job. Pauses any JobRuns and the Job schedule if applicable.



# Resume Job
Source: https://intunedhq.com/docs/client-apis/api-reference/projectsjobs/resume-job

post /{workspaceId}/projects/{projectName}/jobs/{jobId}/resume
Resume a paused Job. Resumes any paused JobRuns and the Job schedule if applicable.



# Trigger Job
Source: https://intunedhq.com/docs/client-apis/api-reference/projectsjobs/trigger-job

post /{workspaceId}/projects/{projectName}/jobs/{jobId}/trigger
Manually trigger a JobRun. Fails if the Job is paused.



# Update Job
Source: https://intunedhq.com/docs/client-apis/api-reference/projectsjobs/update-job

put /{workspaceId}/projects/{projectName}/jobs/{jobId}
Update a Job by ID.



# Get Job Run
Source: https://intunedhq.com/docs/client-apis/api-reference/projectsjobsruns/get-job-run

get /{workspaceId}/projects/{projectName}/jobs/{jobId}/runs/{jobRunId}
Get information and results for a specific JobRun.



# Get Job Runs
Source: https://intunedhq.com/docs/client-apis/api-reference/projectsjobsruns/get-job-runs

get /{workspaceId}/projects/{projectName}/jobs/{jobId}/runs
Get all JobRuns for a Job.



# Terminate Job Run
Source: https://intunedhq.com/docs/client-apis/api-reference/projectsjobsruns/terminate-job-run

post /{workspaceId}/projects/{projectName}/jobs/{jobId}/runs/{jobRunId}/terminate
Terminate a JobRun by ID.



# Run API - Result
Source: https://intunedhq.com/docs/client-apis/api-reference/projectsruns/run-api--result

get /{workspaceId}/projects/{projectName}/run/{runId}/result
Get Run result.



# Run API - Start
Source: https://intunedhq.com/docs/client-apis/api-reference/projectsruns/run-api--start

post /{workspaceId}/projects/{projectName}/run/start
Start a Run for a Project.



# Sink Body
Source: https://intunedhq.com/docs/client-apis/api-reference/sinks/body



## Introduction

When a sink is configured, Run results are delivered to the sink destination. The payload is a JSON object containing the API Run result along with additional metadata. The structure is mostly similar between Jobs and Runs with some minor differences.

## Format

<ParamField type="object">
  Info about the API that was run. Includes the API name and result of run.

  <Expandable>
    <ParamField type="string">
      The API name.
    </ParamField>

    <ParamField type="object | array<any>">
      The parameters to passed to the API for this run.
    </ParamField>

    <ParamField type="object">
      <Tabs>
        <Tab title="Completed">
          <Expandable>
            <ParamField type="enum<string>">
              The status of the API run.

              Available options: `completed`
            </ParamField>

            <ParamField type="any">
              The result of the API run.
            </ParamField>

            <ParamField type="number">
              The status code of the API run.
            </ParamField>
          </Expandable>
        </Tab>

        <Tab title="Failed">
          <Expandable>
            <ParamField type="enum<string>">
              The status of the API run.

              Available options: `failed`
            </ParamField>

            <ParamField type="string">
              Error code.
            </ParamField>

            <ParamField type="string">
              Error message.
            </ParamField>

            <ParamField type="number">
              The status code of the API run.
            </ParamField>
          </Expandable>
        </Tab>
      </Tabs>
    </ParamField>

    <ParamField type="string">
      The run ID for the API run.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField type="string">
  The workspace ID of the project that the run belongs to.
</ParamField>

<ParamField type="object">
  Details of the project that the run belongs to.

  <Expandable>
    <ParamField type="string">
      The project UUID.
    </ParamField>

    <ParamField type="string">
      The project name.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField type="object">
  Details of the auth   used in this run.

  <Expandable>
    <ParamField type="string">
      The auth session ID.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField type="object">
  Present when this Run is part of a Job. Contains information about the Job that triggered it.

  <Expandable>
    <ParamField type="string">
      Job ID.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField type="object">
  Present when this Run is part of a Job. Contains information about the JobRun that triggered it.

  <Expandable>
    <ParamField type="string">
      JobRun ID.
    </ParamField>
  </Expandable>
</ParamField>


# Overview
Source: https://intunedhq.com/docs/client-apis/api-reference/sinks/overview

What are sinks and how to use them

## Introduction

When using a Job or Run to consume your project APIs, you can configure a sink to automatically deliver Run results to a destination. Instead of polling the API for results, sinks push data to you as soon as each Run completes.

Intuned supports two types of sinks:

* **Webhook**: Send Run results to a webhook URL.
* **S3-compatible storage**: Store Run results as JSON files in an S3-compatible bucket.

## Webhook

The webhook sink sends Run results to a webhook URL as a POST request. Use this to deliver data directly to your backend or a third-party service.

For more information, see the [Webhook sink documentation](./webhook).

## S3-compatible storage

The S3 sink stores Run results as JSON files in any S3-compatible storage (AWS S3, Cloudflare R2, Tigris, Supabase Storage, etc.). Use this to persist results and access them later.

For more information, see the [S3 sink documentation](./s3).


# AWS S3 Sink
Source: https://intunedhq.com/docs/client-apis/api-reference/sinks/s3



## Introduction

The S3 sink writes Run results to any S3-compatible storage as JSON files. This is useful for persisting results and accessing them later.

## Usage

S3 sinks can be used with either the [Run API](/docs/client-apis/api-reference/projectsruns/run-api--start) or the [Job API](/docs/client-apis/api-reference/projectsjobs/create-job).

When creating a Run or Job, specify the sink type as `s3` and provide the necessary S3 configuration such as `bucket`, `accessKeyId`, `secretAccessKey`, and `region`. You can also optionally provide a `prefix` to organize files within the bucket. See the Configuration section below for full details on available options.

## Configuration

### Properties

<ParamField type="enum<string>">
  The type of sink.

  Available options: `s3`
</ParamField>

<ParamField type="string">
  The name of the S3 bucket where Run results will be stored.

  Example: `my-s3-bucket`
</ParamField>

<ParamField type="string">
  The access key ID for the S3 bucket.

  Example: `AKIAIOSFODNN7EXSSPLE`
</ParamField>

<ParamField type="string">
  The secret access key for the S3 bucket.

  Example: `wJalrXUtnFFFI/K7MDENG/bsxRfiCYEXAMPLEKEY`
</ParamField>

<ParamField type="string">
  The region where the S3 bucket is located.

  Example: `us-west-2`
</ParamField>

<ParamField type="string">
  Optional prefix for the S3 objects. This can be used to organize objects within the bucket.

  Example: `my-prefix/`
</ParamField>

<ParamField type="boolean">
  If true, failed Runs will not be written to the bucket.
</ParamField>

<ParamField type="array<string>">
  List of API names to include. If not provided, results from all APIs are written.

  Example: `["api1", "api2"]`
</ParamField>

<ParamField type="string">
  Optional custom endpoint for the S3 bucket. This can be used for S3-compatible services.

  Example: `https://s3.custom-endpoint.com`
</ParamField>

<ParamField type="boolean">
  If true, the S3 client will use path-style URLs instead of virtual-hosted-style URLs. This is useful for S3-compatible services that require path-style access.

  Example: `true`
</ParamField>

### Example Configuration

```json theme={null}
{
  // other run/job configuration fields
  ....
  "sink": {
    "type": "s3",
    "bucketName": "my-bucket",
    "accessKeyId": "AKIAIOSFODNN7EXAMPLE",
    "secretAccessKey": "wJalrXUtnFFFI/SSSSSSSbsxRfiCYEXAMPLEKEY",
    "region": "us-west-2",
    "prefix": "my-prefix/",
    "skipOnFail": false
  }
}
```

## File content

Each file is a `.json` file containing the API Run result along with additional metadata. See the [sink body page](./body) for more information on the payload structure.


# Webhook Sink
Source: https://intunedhq.com/docs/client-apis/api-reference/sinks/webhook



## Introduction

The webhook sink delivers Run results to your endpoint via POST request. This is useful for sending results directly to your backend or a third-party service.

## Usage

Webhook sinks can be used with either the [Run API](/docs/client-apis/api-reference/projectsruns/run-api--start) or the [Job API](/docs/client-apis/api-reference/projectsjobs/create-job).

When creating a Run or Job, specify the sink type as `webhook` and provide the destination `url`. You can also optionally provide headers to include in the request. See the Configuration section below for full details.

## Configuration

The webhook sink requires a destination URL. This URL must be publicly accessible. You can optionally add headers to include with each request.

### Properties

<ParamField type="enum<string>">
  The type of sink.

  Available options: `webhook`
</ParamField>

<ParamField type="string">
  The destination URL where Intuned sends Run results.

  Example: `https://example.com/webhook`
</ParamField>

<ParamField type="object">
  Headers to include in the request to your endpoint.

  <Expandable>
    Key-value pairs of header names and values.

    Example:

    ```json theme={null}
    {
      "Content-Type": "application/json",
      "Authorization": "Bearer token"
    }
    ```
  </Expandable>
</ParamField>

<ParamField type="boolean">
  If true, results from failed Runs are not sent.
</ParamField>

<ParamField type="array<string>">
  List of API names to include. If not provided, results from all APIs are sent.

  Example: `["api1", "api2"]`
</ParamField>

### Example Configuration

```json theme={null}
{
  // other run/job configuration fields
  ....
  "sink": {
    "type": "webhook",
    "url": "https://example.com/webhook",
    "headers": {
      "Authorization": "Bearer <token>"
    }
  }
}
```

## API

### Request

Intuned sends a POST request to your endpoint with a JSON payload containing the API Run result and additional metadata. See the [sink body page](./body) for more information on the payload structure.

### Response

The request is considered successful if your endpoint returns a status code in the 200-299 range. If not, the request is considered a failure and Intuned retries with exponential backoff.


# Web Task - Result
Source: https://intunedhq.com/docs/client-apis/api-reference/webtasks/web-task--result

get /{workspaceId}/web-tasks/{webTaskId}/result
Get the status and result of a Web Task. The response is a discriminated union on `status`; the `completed` branch carries an `outcome` plus outcome-specific fields.



# Web Task - Start
Source: https://intunedhq.com/docs/client-apis/api-reference/webtasks/web-task--start

post /{workspaceId}/web-tasks/start
Start a Web Task. The task runs asynchronously; poll the result endpoint for status and output.



# Overview
Source: https://intunedhq.com/docs/client-apis/overview



The Intuned REST API enables you to programmatically trigger automations, manage scheduled Jobs, handle AuthSessions, and run [Web Tasks](/docs/main/07-web-tasks/overview).

You can call the APIs directly as REST endpoints or use our client libraries for [JavaScript](#javascript) and [Python](#python).

## Endpoints

<AccordionGroup>
  <Accordion title="Runs" icon="play">
    Execute your automation APIs on demand.

    * [Start a Run](/docs/client-apis/api-reference/projectsruns/run-api--start) — Trigger an automation and get a Run ID
    * [Get Run result](/docs/client-apis/api-reference/projectsruns/run-api--result) — Retrieve the result of a completed Run
  </Accordion>

  <Accordion title="Jobs" icon="clock">
    Schedule and manage batch executions of your automations.

    * [List Jobs](/docs/client-apis/api-reference/projectsjobs/get-jobs) — Get all Jobs in a Project
    * [Create Job](/docs/client-apis/api-reference/projectsjobs/create-job) — Create a new scheduled Job
    * [Get Job](/docs/client-apis/api-reference/projectsjobs/get-job) — Get Job details
    * [Update Job](/docs/client-apis/api-reference/projectsjobs/update-job) — Modify Job configuration
    * [Delete Job](/docs/client-apis/api-reference/projectsjobs/delete-job) — Remove a Job
    * [Pause Job](/docs/client-apis/api-reference/projectsjobs/pause-job) — Pause a running Job
    * [Resume Job](/docs/client-apis/api-reference/projectsjobs/resume-job) — Resume a paused Job
    * [Trigger Job](/docs/client-apis/api-reference/projectsjobs/trigger-job) — Manually trigger a Job execution
    * [List JobRuns](/docs/client-apis/api-reference/projectsjobsruns/get-job-runs) — Get all JobRuns for a Job
    * [Get JobRun](/docs/client-apis/api-reference/projectsjobsruns/get-job-run) — Get details of a specific JobRun
    * [Terminate JobRun](/docs/client-apis/api-reference/projectsjobsruns/terminate-job-run) — Stop a running Job execution
  </Accordion>

  <Accordion title="AuthSessions" icon="key">
    Create and manage authentication sessions for automations that require login.

    * [List AuthSessions](/docs/client-apis/api-reference/projectsauthsessions/get-authsessions) — Get all AuthSessions in a Project
    * [Get AuthSession](/docs/client-apis/api-reference/projectsauthsessions/get-authsession) — Get AuthSession details
    * [Delete AuthSession](/docs/client-apis/api-reference/projectsauthsessions/delete-authsession) — Remove an AuthSession
    * [Validate AuthSession (start)](/docs/client-apis/api-reference/projectsauthsessionsvalidate/validate-authsession--start) — Start validation of an AuthSession
    * [Validate AuthSession (result)](/docs/client-apis/api-reference/projectsauthsessionsvalidate/validate-authsession--result) — Get validation result
    * [Create AuthSession (start)](/docs/client-apis/api-reference/projectsauthsessionscreate/create-authsession--start) — Start creating an AuthSession
    * [Create AuthSession (result)](/docs/client-apis/api-reference/projectsauthsessionscreate/create-authsession--result) — Get creation result
    * [Update AuthSession (start)](/docs/client-apis/api-reference/projectsauthsessionsupdate/update-authsession--start) — Start updating an AuthSession
    * [Update AuthSession (result)](/docs/client-apis/api-reference/projectsauthsessionsupdate/update-authsession--result) — Get update result
  </Accordion>

  <Accordion title="Web Tasks" icon="wand-magic-sparkles">
    Run repeatable web tasks from a natural-language prompt. No project required. See the [Web Tasks overview](/docs/main/07-web-tasks/overview) for concepts.

    * [Start a Web Task](/docs/client-apis/api-reference/webtasks/web-task--start) — Submit a task and get a `webTaskId`
    * [Get Web Task result](/docs/client-apis/api-reference/webtasks/web-task--result) — Poll for status and retrieve the result
  </Accordion>

  <Accordion title="Sinks" icon="database">
    Configure where Job results are delivered.

    * [Overview](/docs/client-apis/api-reference/sinks/overview) — Learn how sinks work
    * [Webhook](/docs/client-apis/api-reference/sinks/webhook) — Send results to a webhook URL
    * [S3](/docs/client-apis/api-reference/sinks/s3) — Store results in Amazon S3
    * [Body](/docs/client-apis/api-reference/sinks/body) — Include results in the API response
  </Accordion>
</AccordionGroup>

<CardGroup>
  <Card title="OpenAPI Specification" icon="https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/openapi/openapi-plain.svg" href="https://intuned-docs-public-images.s3.us-west-2.amazonaws.com/openapi.yaml" />

  <Card title="Import into Postman" icon="https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/postman/postman-original.svg" href="https://elements.getpostman.com/view/import?apiSchemaUrl=https://intuned-docs-public-images.s3.us-west-2.amazonaws.com/openapi.yaml" />
</CardGroup>

## Prerequisites

All API requests require your workspace ID and an API key.

<CardGroup>
  <Card title="Get your workspace ID" icon="building" href="/docs/03-how-to/manage/manage-workspace#how-to-get-your-workspace-id">
    Find your workspace ID in settings
  </Card>

  <Card title="Manage API keys" icon="key" href="/docs/03-how-to/manage/manage-api-keys">
    Create and manage API keys
  </Card>
</CardGroup>

## Client libraries

### JavaScript

Install the `@intuned/client` package:

<Tabs>
  <Tab title="npm">
    ```bash theme={null}
    npm install @intuned/client
    ```
  </Tab>

  <Tab title="yarn">
    ```bash theme={null}
    yarn add @intuned/client
    ```
  </Tab>

  <Tab title="pnpm">
    ```bash theme={null}
    pnpm add @intuned/client
    ```
  </Tab>
</Tabs>

```typescript theme={null}
import { IntunedClient } from "@intuned/client";

const client = new IntunedClient({
  apiKey: "<YOUR_API_KEY>",
  workspaceId: "<YOUR_WORKSPACE_ID>",
});

// Start an async run
const result = await client.projects.runs.start("my-project", {
  api: "scrape-products",
  parameters: { page: 1 },
});

console.log(result.runId);
```

For [Web Tasks](/docs/main/07-web-tasks/overview), `client.webTasks.run()` submits the task and polls for the result for you:

```typescript theme={null}
const result = await client.webTasks.run({
  task: "Scrape companies from the YC directory",
  parameters: { batch: "S24" },
  reuseKey: "yc_companies",
});

console.log(result.status, result.outcome);
```

### Python

Install the `intuned-client` package:

```bash theme={null}
pip install intuned-client
```

```python theme={null}
from intuned_client import IntunedClient
from intuned_client import models

with IntunedClient(
    workspace_id="<YOUR_WORKSPACE_ID>",
    api_key="<YOUR_API_KEY>",
) as client:
    # Start an async run
    result = client.projects.runs.start(
        project_name="my-project",
        body=models.RunStartRequestBody(
            api="scrape-products",
            parameters={"page": 1},
        ),
    )
    print(result.run_id)
```

## Rate limits

Intuned APIs are rate-limited to 60 requests per minute per URI path.

<Tip>
  If you need a higher rate limit, [contact us](/docs/06-resources/help-and-support).
</Tip>


# How the platform works
Source: https://intunedhq.com/docs/main/00-getting-started/how-intuned-works

Understand Projects, Runs, Jobs, and the execution model

Intuned is a browser automation platform for developers. You write browser automations as code, deploy them to our infrastructure, and execute them on demand or on a schedule—with built-in [authentication](/docs/main/02-features/auth-sessions), [stealth](/docs/main/02-features/stealth-mode-captcha-solving-proxies), and scale.

<Note>
  This page describes **Intuned Projects**—building and deploying code-first browser automations. If you just want an API that runs a web task for you, see [Web Tasks](/docs/main/07-web-tasks/overview).
</Note>

## Projects

A **Project** is both your automation code and your runtime environment.

**As code**, a Project contains your browser automation APIs and shared logic—written in **Python** or **TypeScript** using Playwright.

```
Project/
├── apis/
│   ├── scrape-product.ts    # Your automation APIs
│   └── submit-form.ts
├── helpers/                  # Shared utilities
└── Intuned.json       # Project configuration
```

**At runtime**, a Project is where everything comes together: [Run](/docs/main/02-features/runs-single-executions) records, [Jobs](/docs/main/02-features/jobs-batched-executions), JobRuns, browser [traces and logs](/docs/main/02-features/observability-monitoring-logs), and [AuthSessions](/docs/main/02-features/auth-sessions) all live within the Project that owns them.

Each **API** is a function that receives a browser page and parameters, performs actions, and returns results. You have full control over the code—use any library, pattern, or approach that fits your needs.

## Building your automations

You can work on every Intuned Project in the [online IDE](/docs/main/02-features/online-ide), with the [local CLI](/docs/main/02-features/local-development-cli), or with both.

In both approaches, you write browser automation code in Python or TypeScript, organize it into APIs, test it, and deploy it to Intuned. The main difference is **where you develop** and **how your team works with the code**.

### Online IDE

The online IDE lets you build directly in Intuned's browser-based editor or with Intuned Agent.

* **How it works**: Create or open a project, edit files in the browser, run APIs with the built-in browser view, and deploy when ready.
* **Best for**: Fast starts, solo development, quick prototyping, and teams that want platform-native workflows with minimal setup.
* **Key advantages**: Zero local setup, built-in AI assistance, Selector Agent, browser-based debugging, and access to platform AI features such as **Fix with AI**, **Fix issues**, and **self-healing projects**.
* **Tradeoff**: You work inside Intuned's editor rather than your own local IDE, and Git-based collaboration is more limited.

### Local CLI

The local CLI lets you build the same project from your machine and sync it with Intuned.

* **How it works**: Create a project with the local CLI, open it in your preferred editor, manage the code in your own repo, run and test locally, then deploy to Intuned when ready.
* **Best for**: Teams using Git workflows, CI/CD pipelines, code review, custom local tooling, and shared repositories.
* **Key advantages**: Full version control, local IDE choice, easier team collaboration, and better fit for existing engineering workflows.
* **Tradeoff**: You need local setup and dependency management. You need to sync your changes locally if they are modified on the platform.

<Tip>
  **[Intuned Agent](/docs/main/02-intuned-agent/overview)** can build entire Projects from a prompt, edit existing ones, fix failed Runs, and [maintain your automations automatically](/docs/main/02-intuned-agent/self-healing-projects) after deployment.
</Tip>

## Deployment

When you deploy a Project, all its APIs become available as callable endpoints. Intuned handles the infrastructure: browser provisioning, execution queues, retries, and scaling.

## Execution model

### Runs and Attempts

When you call an API, you create a **Run**—a single logical execution of your automation.

Each Run may include one or more **Attempts**. If an Attempt fails (network issues, transient errors), Intuned can automatically retry up to your configured limit. Every Attempt is tracked with its own [logs and browser trace](/docs/main/02-features/observability-monitoring-logs), plus timing data.

```
Run
├── Attempt 1 → Failed (timeout)
├── Attempt 2 → Failed (element not found)
└── Attempt 3 → Success ✓
```

### Two ways to execute

**[Runs](/docs/main/02-features/runs-single-executions)** — On-demand execution via API. You trigger a Run, we handle queuing and concurrency.

**[Jobs](/docs/main/02-features/jobs-batched-executions)** — Scheduled or batch execution. Define what to run, when to run it, and where to send results. Built for scrapers and crawlers that need to run on a recurring basis. Each execution of a Job is a **JobRun**—the Job is the template, and each JobRun is a single execution of that template.

## Authentication

For automations that require login, Intuned provides built-in [authentication support](/docs/main/02-features/auth-sessions). You define two functions—`create` (how to log in) and `check` (how to verify a session is valid)—and Intuned handles the rest: validating sessions before Runs, reusing them when possible, and recreating them when expired.

## Key features

**[Flexible automations](/docs/main/02-features/flexible-automation)** — Deploy any code. Deterministic, AI-driven, or hybrid—use any library or package. Unopinionated by design.

**[Built-in authentication](/docs/main/02-features/auth-sessions)** — Write `create` and `check` functions, and Intuned handles the rest—validating, reusing, and recreating sessions automatically.

**[Online IDE](/docs/main/02-features/online-ide)** — Zero setup. Write, test, and deploy directly from your browser.

**[Stealth and anti-detection](/docs/main/02-features/stealth-mode-captcha-solving-proxies)** — Run automations without getting blocked. Stealth mode, automatic captcha solving, and proxy support are built in.

**[Observability](/docs/main/02-features/observability-monitoring-logs)** — Full logs, traces, and session recordings for every Run. See what happened and why.

## What's next?

* **[Build a scraper with Intuned Agent](/docs/main/00-getting-started/quickstarts/intuned-agent)** — Generate a scraper from a prompt and schema
* **[Build an RPA with Intuned Agent](/docs/main/00-getting-started/quickstarts/build-an-rpa-with-intuned-agent)** — Automate browser workflows using the agent
* **[Scraper quickstart](/docs/main/00-getting-started/quickstarts/scraper)** — Build a scraper by hand, deploy it, configure a Job, and run it end to end. Covers the platform concepts from this page in practice.
* **[RPA quickstart](/docs/main/00-getting-started/quickstarts/rpa)** — Build an RPA automation by hand, deploy it, and trigger it via API. A hands-on walkthrough of the platform workflow.
* **[Intuned in depth](/docs/main/01-learn/deep-dives/intuned-indepth)** — Explore the platform in depth


# Introduction
Source: https://intunedhq.com/docs/main/00-getting-started/introduction

Code-first browser automation platform with an AI agent that builds and maintains your automations

<Frame>
  <iframe />
</Frame>

## What is Intuned?

Intuned is a code-first browser automation platform for developers, with two parts:

1. **A platform** for deploying and running browser automations as code—with built-in authentication, stealth, monitoring, scheduling, and scale.
2. **An AI agent** that builds, edits, and maintains those automations autonomously.

Build automations by hand or let AI build them for you. What deploys is always code you can read, debug, and own. Because the agent runs inside the platform—with access to browser traces, run history, and project configuration—it can do more than generate code. It can test against live pages, diagnose failures, and maintain your automations after deployment.

## Two ways to use Intuned

<CardGroup>
  <Card title="Projects" icon="folder-tree" href="/main/00-getting-started/how-intuned-works">
    Build, deploy, and run browser automations as code. Full platform:
    AuthSessions, Jobs, the IDE, the CLI, and Intuned Agent. Best when you want
    to own and maintain the automation.
  </Card>

  <Card title="Web Tasks" icon="wand-magic-sparkles" href="/main/07-web-tasks/overview">
    Describe a web task in plain English and call an API. No project to create
    or maintain. Best for repeatable tasks with a clear starting URL—runs get
    cheaper and faster over time.
  </Card>
</CardGroup>

## What you can build

Browser automation covers any task where software controls a browser to interact with websites. Common use cases include:

**Scrapers** — Extract structured data from websites. Product prices, job listings, public records, news articles. Write the extraction logic once, run it on a schedule, and get results delivered to your systems.

**RPA automations** — Interact with websites on behalf of users. Form submissions, data entry, account actions, report downloads. Useful when a service doesn't offer an API or you need to automate legacy web apps.

**Crawlers** — Discover and collect data across many pages. Follow links, parse sitemaps, index content. Build crawlers that process hundreds or thousands of pages per run.

**AI-powered automations** — Use LLMs to navigate, interact, and extract from websites. Build AI agents that adapt to different sites or combine AI with deterministic code for reliability where it matters.

## Intuned Agent

Intuned Agent is an AI agent that builds, edits, and maintains browser automation Projects. It runs inside the Intuned platform with a real browser, full access to the platform, and works in the background as long as needed.

**Build from a conversation** — Describe your scraper or automation in natural language. The agent creates a plan, controls a real browser in the background, validates against live pages, and delivers working code with sample data. Typically takes 30–60 minutes.

**Edit existing projects** — Select a project, describe the change. Add fields, fix selectors, change pagination, modify formatting. The agent shows a diff, tests it, and delivers results.

**Fix failed runs** — Click "Fix with AI" on any failed run. Error context is pre-filled—the agent diagnoses and generates a fix without back-and-forth.

**Self-healing projects** — Enable self-healing on any project. Intuned detects issues automatically—not just pass/fail, but anomalies in response size, output patterns, and run behavior. The agent can [fix issues, merge changes, and redeploy](/docs/main/02-intuned-agent/self-healing-projects), with the level of autonomy you choose.

**Run it locally** — Prefer your own setup? The [External agent](/docs/main/02-intuned-agent/external-agent) (private preview) runs the same agent in Claude Code or Codex to build, edit, and debug automations locally.

<CardGroup>
  <Card title="Try Intuned Agent" icon="wand-magic-sparkles" href="https://app.intuned.io/agent">
    Start building with Intuned Agent
  </Card>

  <Card title="Learn more" icon="book" href="/main/02-intuned-agent/overview">
    See what the agent can do
  </Card>
</CardGroup>

## Platform features

**[Built-in authentication](/docs/main/02-features/auth-sessions)** — Handle login flows with AuthSessions. You define how to log in and verify sessions—Intuned manages the rest.

**[Stealth and anti-detection](/docs/main/02-features/stealth-mode-captcha-solving-proxies)** — Proxies, stealth mode, and CAPTCHA solving help your automations avoid detection.

**[Monitoring and traces](/docs/main/02-features/observability-monitoring-logs)** — Full logs, browser traces, and session recordings for every Run.

**[Jobs and scheduling](/docs/main/02-features/jobs-batched-executions)** — Run automations on a schedule or in batches. Configure retries, concurrency, and result delivery.

**[Flexible automations](/docs/main/02-features/flexible-automation)** — Write deterministic scripts, AI-driven automations, crawlers, or any combination. Use any library or pattern.

## Quickstarts

<CardGroup>
  <Card title="Build a scraper with AI" icon="wand-magic-sparkles" href="/main/00-getting-started/quickstarts/intuned-agent">
    Generate a scraper from a prompt and schema.
  </Card>

  <Card title="Build an RPA with AI" icon="robot" href="/main/00-getting-started/quickstarts/build-an-rpa-with-intuned-agent">
    Automate browser workflows using the agent.
  </Card>

  <Card title="Build a scraper manually" icon="database" href="/main/00-getting-started/quickstarts/scraper">
    Extract data from a website with code.
  </Card>

  <Card title="Build an RPA manually" icon="robot" href="/main/00-getting-started/quickstarts/rpa">
    Automate browser workflows with code.
  </Card>

  <Card title="Authenticated RPA" icon="lock" href="/main/00-getting-started/quickstarts/auth-rpa">
    Automate login-protected workflows with AuthSessions.
  </Card>
</CardGroup>

## Continue reading

<CardGroup>
  <Card title="How the platform works" icon="book" href="/main/00-getting-started/how-intuned-works">
    Understand Projects, Runs, Jobs, and the execution model
  </Card>

  <Card title="Intuned vs other approaches" icon="scale-balanced" href="/main/01-learn/deep-dives/intuned-vs-others">
    See how Intuned compares to other browser automation tools
  </Card>
</CardGroup>

<Note>
  **Need managed automations?** Our solution engineers—powered by Intuned
  Agent—build and maintain automations for you. Tell us what you need, and we
  handle the rest. [Get in touch](https://cal.com/team/intuned/get-started).
</Note>


# Quickstart: Build an authenticated RPA with code
Source: https://intunedhq.com/docs/main/00-getting-started/quickstarts/auth-rpa



In this quickstart, you'll build an RPA (Robotic Process Automation) that automates booking a consultation appointment on a website that requires login. You'll use AuthSessions—Intuned's built-in authentication management feature—to create and manage the authentication state. By the end, you'll have a working authenticated automation ready to run on demand.

## Prerequisites

* An active Intuned account ([sign up here](https://app.intuned.io)). No credit card required—Intuned has a free plan
* Basic familiarity with TypeScript or Python

## Create and deploy your first authenticated RPA

You can develop Intuned Projects in two ways:

* **Online IDE** — Start with no local setup. Write, test, and deploy from your browser.
* **Local CLI** — Develop locally with version control and CI/CD integration.

Choose your preferred approach below.

<Tabs>
  <Tab title="Online IDE">
    <Steps>
      <Step title="Log in and create project" icon="plus">
        1. Go to [app.intuned.io/projects](https://app.intuned.io/projects) and log in.
        2. Select **Create Project**.
        3. Select your language (TypeScript or Python).
        4. Choose the **rpa-auth-example** template.
        5. Name it `book-consultations-authenticated-quickstart`.
        6. Select **Start in Intuned**.
        7. Select **Create and Open**.

        <img alt="Create RPA with Auth Project" />

        **Expected result:** The Intuned IDE opens with your project loaded.

        > **What you just got:** An **Intuned Project** groups related browser automations together. Each file in the `api/` folder becomes a callable function that controls a browser using Playwright, accepts parameters, and returns results. When working with sites that require login, you can create AuthSessions that store and manage login states, allowing your automation APIs to run authenticated actions without logging in every time. When you deploy the project, all its APIs go live together as a single deployable unit.

        <Tip>
          AuthSessions are a powerful feature of Intuned that simplify handling authentication in your automations. By defining login flows and session checks, you ensure your automations always have valid access to the target site. Learn more in the [Authentication documentation](/docs/main/02-features/auth-sessions).
        </Tip>
      </Step>

      <Step title="Explore the project code" icon="code">
        In the file explorer, you'll see the API file:

        **`api/book-consultations`** - A complete automation that:

        * Navigates to the booking form
        * Fills in personal details (name, email, phone)
        * Selects date, time, and consultation topic
        * Submits the form and verifies success

        **How it works:** This automation controls a browser like a human would—navigating to a website, filling form fields, clicking buttons—but executes these actions automatically based on your code and input parameters.

        Also, you'll notice a special folder named `auth-sessions/` that contains two special APIs:

        * **`auth-sessions/create`** - Defines how to perform your login flow. It receives credentials as input parameters and automates your login process, saving the captured browser state and cookies to be reused later.
        * **`auth-sessions/check`** - Checks if the current session is still valid or if a new login is required. This API executes automatically before every Run, ensuring your automation always has a valid authenticated session.
      </Step>

      <Step title="Validate your AuthSession" icon="shield-check">
        First, let's set up authentication. You'll have a pre-created test AuthSession with credentials that you can use. Let's run an **AuthSession:Validate** Run that will run the `check` API; and if it fails, it will automatically run the `create` API to log in and create a new session state.

        In the top toolbar:

        1. Select **AuthSession: Validate** from the API dropdown.
        2. Select **test-authsession** from the AuthSession dropdown.
        3. Select the **Run** button.

           <img alt="Run AuthSession validate" />

        **Expected result:** You'll notice that the `check` API runs and fails (no session state exists yet), then the `create` API runs to log in and create a new session, and a subsequent `check` API runs and passes. Your AuthSession is now ready to be used by your automations in the IDE!

        <Tip>
          The credentials are pre-configured in the auth session instance for the demo site. When building your own authenticated automations, you'll pass your own credentials when creating your auth session instance.
        </Tip>
      </Step>

      <Step title="Run your automation in the IDE" icon="play">
        Test the automation to see it working right in the IDE.

        1. Select **API: book-consultations** from the API dropdown.
        2. Make sure the **test-authsession** AuthSession is selected as well, so the API can run authenticated with this valid session.
        3. Select **Web Scraping Consultation** next to it—you'll see pre-filled test data already configured.
        4. Select the **Run** button.

           <img alt="Run RPA ide" />

        **Expected result:** The browser panel on the right shows the automation executing live. First, it will rerun the `check` API to verify your AuthSession. You'll see it navigate to the booking form, fill in all fields, submit, and verify success. The terminal below displays the result of the Run.

        <Expandable title="result of the Run">
          ```json theme={null}
          {
            "input": {
              "name": "John Smith",
              "email": "john.smith@example.com",
              "phone": "+1-555-0123",
              "date": "2024-12-25",
              "time": "14:30",
              "topic": "web-scraping"
            },
            "output": {
              "success": true,
              "date": "2024-12-25",
              "message": "Consultation successfully booked for 2024-12-25 at 14:30"
            }
          }
          ```
        </Expandable>
      </Step>

      <Step title="Deploy your project" icon="rocket">
        Deploy your automation to Intuned's infrastructure.

        1. Select the **Deploy** button in the top-right corner of the IDE.
        2. Leave **Create test AuthSession** toggle selected. This will create a `test-auth-session` AuthSession so you can use for testing.
        3. In the deployment dialog, select **Deploy** to start.
        4. Watch the live deployment logs until you see "Ready".

        **Expected result:** A success message appears. Your automation is now live and ready to run.
      </Step>

      <Step title="Test in the Playground" icon="circle-check">
        Now test your deployed authenticated automation through the API Playground.

        1. In the deployment success dialog, select **Run in Playground**.
        2. In the Playground, you'll see your deployed API **book-consultations** ready to call. The newly created test AuthSession is selected.
        3. The request body is pre-filled with test parameters. Select **Start Run** to execute.

        <Tip>
          The Playground is just an interactive way to test your deployed automation APIs. Your automation is now callable from anywhere via API—use it from your application, service, or any HTTP client. See the [API Reference](/docs/client-apis/api-reference) for authentication and programmatic usage.
        </Tip>

        <img alt="Playground RPA run" />

        **Expected result:** The automation executes on Intuned's infrastructure. You'll see the run details, AuthSession validation, and results in real-time.

        Your automation is now deployed and fully operational.
      </Step>
    </Steps>
  </Tab>

  <Tab title="Local CLI">
    <Steps>
      <Step title="Install and create project" icon="terminal">
        Open your terminal and run:

        ```bash theme={null}
            npx create-intuned-project@latest
        ```

        When prompted:

        1. **Project name:** Enter `book-consultations-authenticated-quickstart`.
        2. **Language:** Select **TypeScript** or **Python**.
        3. **Template:** Choose **rpa-auth-example**.
        4. **Your Workspace Id:** Check [here](/docs/main/03-how-to/manage/manage-workspace) how to find it.
        5. **Your API Key**: Check [here](/docs/main/03-how-to/manage/manage-api-keys) how to create one.

        **Expected result:** The CLI creates a new folder `book-consultations-authenticated-quickstart/` with the project structure and installs dependencies.

        > **What you just got:** An **Intuned Project** groups related browser automations together. Each file in the `api/` folder becomes a callable function that controls a browser using Playwright, accepts parameters, and returns results. When working with sites that require login, you can create AuthSessions that store and manage login states, allowing your automation APIs to run authenticated actions without logging in every time. When you deploy the project, all its APIs go live together as a single deployable unit.

        <Tip>
          AuthSessions are a powerful feature of Intuned that simplify handling authentication in your automations. By defining login flows and session checks, you ensure your automations always have valid access to the target site. Learn more in the [Authentication documentation](/docs/main/02-features/auth-sessions).
        </Tip>
      </Step>

      <Step title="Navigate to project directory" icon="folder">
        ```bash theme={null}
            cd book-consultations-authenticated-quickstart
        ```
      </Step>

      <Step title="Explore the project code" icon="code">
        Open the project in your code editor. You'll see the API file:

        **`api/book-consultations`** - A complete automation that:

        * Navigates to the booking form
        * Fills in personal details (name, email, phone)
        * Selects date, time, and consultation topic
        * Submits the form and verifies success

        **How it works:** This automation controls a browser like a human would—navigating to a website, filling form fields, clicking buttons—but executes these actions automatically based on your code and input parameters.

        Also, you'll notice a special folder named `auth-sessions/` that contains two special APIs:

        * **`auth-sessions/create`** - Defines how to perform your login flow. It receives credentials as input parameters and automates your login process, saving the captured browser state and cookies to be reused later.
        * **`auth-sessions/check`** - Checks if the current session is still valid or if a new login is required. This API executes automatically before every Run, ensuring your automation always has a valid authenticated session.
      </Step>

      <Step title="Validate your AuthSession" icon="shield-check">
        First, let's set up authentication. When you created your project, it came with a pre-created test AuthSession under the `/auth-sessions-instances` folder with credentials that you can use. Let's run a validate command to recreate the session state.

        Run the following command to validate the AuthSession:

        <CLICommandTabs />

        **Expected result:** The CLI runs the `check` API and fails (no session state exists yet), then runs the `create` API to log in and create a new session, and runs a subsequent `check` API that passes. Your AuthSession is now ready to be used by your automations.

        <Tip>
          The credentials are pre-configured in the auth session instance for the demo site. When building your own authenticated automations, you'll pass your own credentials when creating your auth session instance.
        </Tip>
      </Step>

      <Step title="Run your automation locally" icon="play">
        Test the automation to see it working on your machine.

        Run the following command:

        <CLICommandTabs />

        **Expected result:** The CLI launches a browser in the background. First, it reruns the `check` API to verify your AuthSession. You'll see it navigate to the booking form, fill in all fields, submit, and verify success. The results appear in your terminal.

        <Expandable title="result of the Run">
          ```json theme={null}
          {
            "result": {
              "success": true,
              "date": "2025-12-10",
              "message": "Consultation successfully booked for 2025-12-10 at 14:30"
            }
          }
          ```
        </Expandable>
      </Step>

      <Step title="Deploy your project" icon="rocket">
        Deploy your automation to Intuned's infrastructure.
        Run the following command to deploy:

        <CLICommandTabs />

        <Note>
          If your project uses environment variables or secrets, configure them in Intuned before deploying. Local `.env` files are only used during local development and are not included in deployments. Set shared values in [workspace environment variables](/docs/main/02-features/environment-variables-secrets#workspace-level-environment-variables) or add them at the project level if they should only apply to this project.
        </Note>

        When prompted, accept creating a test AuthSession. This will create a `test-authsession` AuthSession so you can use for testing.

        **Expected result:** You see deployment progress logs as the CLI deploys your project and creates the test AuthSession. When complete, you'll see:

        ```bash theme={null}
        ✓ Deployment successful
        ```

        Your automation is now live and ready to run.
      </Step>

      <Step title="Test in the Playground" icon="circle-check">
        Now test your deployed authenticated automation through the API Playground.

        1. Navigate to [Playground](https://app.intuned.io/playground) in your browser.
        2. Select your project **book-consultations-authenticated-quickstart** and the API **book-consultations**.
        3. The newly created test AuthSession is selected.
        4. The request body is pre-filled with test parameters. Select **Start Run** to execute.

        <Tip>
          The Playground is just an interactive way to test your deployed automation APIs. Your automation is now callable from anywhere via API—use it from your application, service, or any HTTP client. See the [API Reference](/docs/client-apis/api-reference) for authentication and programmatic usage.
        </Tip>

        <img alt="Playground RPA run" />

        **Expected result:** The automation executes on Intuned's infrastructure. You'll see the run details, AuthSession validation, and results in real-time.

        Your automation is now deployed and fully operational.
      </Step>
    </Steps>
  </Tab>
</Tabs>

## What's next?

* **[Online IDE](/docs/main/02-features/online-ide)** — Learn more about the Intuned IDE used in this quickstart.
* **[Local development (CLI)](/docs/main/02-features/local-development-cli)** — Learn more about the Intuned CLI used in this quickstart.
* **[Authentication](/docs/main/02-features/auth-sessions)** — Learn more about AuthSessions, how to create them, and best practices for handling authentication in your automations.
* **[Monitoring and traces](/docs/main/02-features/observability-monitoring-logs)** — Every run generates detailed logs, browser traces, and session recordings. Use these tools to debug failures, verify your automation is working correctly, and understand what happened during execution.
* **[Flexible automations](/docs/main/02-features/flexible-automation)** — Build automations your way. Write deterministic code, use AI-driven extraction, or combine both in a hybrid approach. Use any library or package—Intuned is unopinionated by design.
* **[Intuned Agent quickstart](/docs/main/00-getting-started/quickstarts/intuned-agent)** — You can write your automation logic manually like in this quickstart, or use Intuned Agent to generate automations from a prompt and schema. Intuned Agent can also help you update existing automations, fix failed runs, and iterate on your code faster.
* **[Cookbook](/docs/main/01-learn/cookbook)** — Browse full working examples of automations and scrapers. Each example includes complete code you can use as a starting point for your own projects.


# Quickstart: Build an RPA with Intuned Agent
Source: https://intunedhq.com/docs/main/00-getting-started/quickstarts/build-an-rpa-with-intuned-agent



In this quickstart, you'll use Intuned Agent to build an RPA that automates patient management on an OpenEMR demo instance. By the end, you'll have a working automation that can list patients and add new ones—all generated from a simple prompt.

## What is Intuned Agent?

Intuned Agent is an AI agent that builds, edits, and maintains browser automation Projects. It runs inside the Intuned platform with a real browser, full access to the platform via the [Intuned CLI](/docs/main/05-references/cli/overview), and works in the background as long as needed.

Learn more about Intuned Agent's full capabilities in the [overview](/docs/main/02-intuned-agent/overview).

## Prerequisites

* An active Intuned account ([sign up here](https://app.intuned.io)). No credit card required—Intuned has a free plan.

## What you'll build

You'll instruct Intuned Agent to build an RPA that automates [OpenEMR's demo instance](https://demo.openemr.io/openemr/interface/login/login.php?site=default). The agent will build:

* AuthSessions for secure login to the OpenEMR portal
* An API to retrieve the list of existing patients
* An API to add a new patient

## Create your first RPA

<Steps>
  <Step title="Enter your prompt" icon="wand-magic-sparkles">
    1. Go to [app.intuned.io/agent](https://app.intuned.io/agent).
    2. Paste the prompt below into the input and send it.

    ```md Prompt theme={null}
    I want to automate the following website: https://demo.openemr.io/openemr/interface/login/login.php?site=default
    - I want to get the list of patients.
    - I want to be able to add a new patient.
    ```

    <Frame>
      <img alt="Intuned Agent session starting" />
    </Frame>

    **Expected result:** The agent starts a session and begins exploring the target website.
  </Step>

  <Step title="Choose your language" icon="code">
    The agent asks which language you'd like to use for the project. Select **TypeScript** or **Python**.

    <Frame>
      <img alt="Agent asking which language to use" />
    </Frame>
  </Step>

  <Step title="Enter credentials" icon="lock">
    The agent detects that the site requires login and asks for credentials. Values are saved securely and never shared with the AI agent.

    1. **Username:** `admin`
    2. **Password:** `pass`
    3. Select **Save Credentials**.

    <Frame>
      <img alt="Enter credentials dialog" />
    </Frame>

    <Tip>Credentials are saved securely and never shared with the AI agent. For production use, configure credentials as environment variables or use Intuned's [auth sessions](/docs/main/02-features/auth-sessions).</Tip>
  </Step>

  <Step title="Answer the agent's questions" icon="comments">
    The agent explores the site — navigating pages, inspecting forms, and analyzing the data structure. It then asks clarifying questions to finalize the design, such as whether the "get patients" API should support filtering or return all patients. Answer each question as it appears.

    <Frame>
      <img alt="Agent asking clarifying questions" />
    </Frame>
  </Step>

  <Step title="Approve the plan" icon="clipboard-check">
    Once the agent has finished exploring, it presents a full plan including configuration (start URL, auth, language, template), discovered URLs, and the API structure. Review it and select **Approve Plan** to proceed, or **Suggest Changes** to adjust.

    <Frame>
      <img alt="Agent plan ready for approval" />
    </Frame>

    **Expected result:** The agent begins building your automation.
  </Step>

  <Step title="Wait for the agent to build" icon="gears">
    The agent writes the code, runs local and E2E platform tests, and validates the output. This typically takes 30–60 minutes.
  </Step>

  <Step title="Review results and deploy" icon="rocket">
    When the agent finishes, it shows a work summary describing what was built and the test results. In this case, the agent built two APIs (`get-patients` and `add-patient`), both with auth session support and passing E2E tests.

    Select **View Code** to inspect the generated code, or **Merge Branch** to deploy.

    <Frame>
      <img alt="Agent work summary with merge and view code options" />
    </Frame>

    When you're satisfied, select **Merge Branch**. In the dialog, choose **Merge & Deploy** to create an Intuned project and deploy it immediately, or **Merge only** to merge the code without deploying.

    <Frame>
      <img alt="Merge and deploy dialog" />
    </Frame>

    **Expected result:** Your automation is deployed and ready to run via API or direct triggers.
  </Step>

  <Step title="Run your automation" icon="circle-check">
    Once deployed, navigate to your project and select the **Runs** tab. Select **Start Run** to trigger your automation.

    <Frame>
      <img alt="Project runs page with Start Run button" />
    </Frame>

    **Expected result:** The run completes successfully. You can see the input parameters, output result, auth session validation, and the full attempts timeline.

    <Frame>
      <img alt="Successful add-patient run with parameters and result" />
    </Frame>
  </Step>

  <Step title="(Optional) Iterate on your automation" icon="rotate">
    Start a new conversation and select **Pick project** to load your automation. You can then ask the agent to add new APIs, adjust form fields, handle edge cases, or update the login flow.

    **Example prompt:**

    ```markdown theme={null}
    Add a new API called "get-appointments" that takes a date and returns
    all appointments scheduled for that day.
    ```
  </Step>
</Steps>

## What's next?

* **[Intuned Agent](/docs/main/02-intuned-agent/overview)** — Learn more about Intuned Agent's capabilities, including editing existing projects and fixing failed runs.

* **[Authentication](/docs/main/02-features/auth-sessions)** — For automations that require login, Intuned provides built-in authentication support. You define how to log in and how to verify a session, and Intuned handles the rest—validating sessions before runs, reusing them when possible, and recreating them when expired.

* **[Monitoring and traces](/docs/main/02-features/observability-monitoring-logs)** — Every run generates detailed logs, browser traces, and session recordings. Use these tools to debug failures, verify your automation is working correctly, and understand what happened during execution.

* **[Online IDE](/docs/main/02-features/online-ide)** — Learn more about the Intuned IDE, which you can use to manually edit the automation you just created.


# Quickstart: Build a scraper with Intuned Agent
Source: https://intunedhq.com/docs/main/00-getting-started/quickstarts/intuned-agent



In this quickstart, you'll use Intuned Agent to build a scraper that extracts job postings from Apple's career page. By the end, you'll have a working scraper that handles pagination and extracts structured data—all generated from a simple prompt.

## What is Intuned Agent?

Intuned Agent is an AI agent that builds, edits, and maintains browser automation Projects. It runs inside the Intuned platform with a real browser, full access to the platform via the [Intuned CLI](/docs/main/05-references/cli/overview), and works in the background as long as needed.

Learn more about Intuned Agent's full capabilities in the [overview](/docs/main/02-intuned-agent/overview).

## Prerequisites

* An active Intuned account ([sign up here](https://app.intuned.io)). No credit card required—Intuned has a free plan.

## What you'll build

You'll instruct Intuned Agent to build a scraper that scrapes [Apple's career page](https://jobs.apple.com/en-us/search?location=united-states-USA). The scraper will:

* Extract job postings with a defined JSON schema
* Paginate through the full job list
* Capture detailed information for each posting

## Create your first scraper

<Steps>
  <Step title="Enter your prompt" icon="wand-magic-sparkles">
    1. Go to [app.intuned.io/agent](https://app.intuned.io/agent).
    2. Paste the prompt below into the input and send it.

    ```md Prompt theme={null}
    I want to scrape job postings from [Apple career page](https://jobs.apple.com/en-us/search?location=united-states-USA)

    FILTER: No need to apply any filters

    For each job, I need:

    job_title: string
    product_and_service: string
    post_date: string (iso format)
    description: string
    summary: string
    apply_url: string
    role_number: string
    ```

    <Frame>
      <img alt="Intuned Agent session starting" />
    </Frame>

    **Expected result:** The agent starts a session and begins exploring the target website.
  </Step>

  <Step title="Answer the agent's questions" icon="comments">
    As the agent explores the site, it asks clarifying questions to shape the scraper—such as which language to use, how to handle pagination, and what to name the project. Answer each question as it appears.

    <Frame>
      <img alt="Agent asking which language to use" />
    </Frame>

    <Tip>When asked about pagination, **Configurable max pages** is the recommended option—it gives you control at runtime without rebuilding the scraper.</Tip>
  </Step>

  <Step title="Approve the plan" icon="clipboard-check">
    Once the agent has finished exploring, it presents a full plan: the start URL, entity structure, navigation instructions, and schema. Review it and select **Approve Plan** to proceed, or **Suggest Changes** to adjust.

    <Frame>
      <img alt="Agent plan ready for approval" />
    </Frame>

    **Expected result:** The agent begins building your scraper.
  </Step>

  <Step title="Wait for the agent to build" icon="gears">
    The agent writes the code, runs end-to-end tests, and validates the output. This typically takes 30–60 minutes.

    <Frame>
      <img alt="Agent building and running E2E tests" />
    </Frame>
  </Step>

  <Step title="Review results and deploy" icon="rocket">
    When the agent finishes, it shows a work summary with a description of what was built and the E2E test results. Select **View Code** to inspect the generated code, or **Merge Branch** to deploy.

    <Frame>
      <img alt="Agent work summary" />
    </Frame>

    Select **View Code** and open the **Artifacts** tab to inspect the actual scraped data from the agent's test run before deploying.

    <Frame>
      <img alt="Artifacts panel showing scraped results" />
    </Frame>

    When you're satisfied, select **Merge Branch**. In the dialog, choose **Merge & Deploy** to create an Intuned project and deploy it immediately, or **Merge only** to merge the code without deploying.

    <Frame>
      <img alt="Merge and deploy dialog" />
    </Frame>

    **Expected result:** Your scraper is deployed and ready to run via API, scheduled Jobs, or direct triggers.
  </Step>

  <Step title="Run your scraper" icon="circle-check">
    Once deployed, navigate to your project and select the **Runs** tab. Select **Start Run** to trigger your scraper.

    <Frame>
      <img alt="Project runs page with Start Run button" />
    </Frame>

    **Expected result:** The run completes successfully. You can see the input parameters, output result, and the full attempts timeline.

    <Frame>
      <img alt="Successful scraper run showing extracted job postings" />
    </Frame>
  </Step>

  <Step title="(Optional) Iterate on your scraper" icon="rotate">
    Start a new conversation and select **Pick project** to load your scraper. You can then ask the agent to add filters, change the schema, fix selectors, or adjust the scraping logic.

    **Example prompt:**

    ```markdown theme={null}
    Add a parameter to filter by product and service. Examples:
    Apple Ads
    AirPods
    ```
  </Step>
</Steps>

## What's next?

* **[Intuned Agent](/docs/main/02-intuned-agent/overview)** — Learn more about Intuned Agent's capabilities, including editing existing projects and fixing failed runs.

* **[Jobs](/docs/main/02-features/jobs-batched-executions)** — Jobs are the common way to run scrapers. Configure a schedule (daily, hourly, or custom) and define a sink to send your scraper results to a webhook, S3 bucket, or other destination.

* **[Authentication](/docs/main/02-features/auth-sessions)** — For scrapers that require login, Intuned provides built-in authentication support. You define how to log in and how to verify a session, and Intuned handles the rest—validating sessions before runs, reusing them when possible, and recreating them when expired.

* **[Monitoring and traces](/docs/main/02-features/observability-monitoring-logs)** — Every run generates detailed logs, browser traces, and session recordings. Use these tools to debug failures, verify your scraper is working correctly, and understand what happened during execution.

* **[Online IDE](/docs/main/02-features/online-ide)** — Learn more about the Intuned IDE, which you can use to manually edit the scraper you just created.

* **[External agent](/docs/main/02-intuned-agent/external-agent)** — Prefer your own setup? The external agent (private preview, available on request) runs the same agent locally in Claude Code or Codex so you can build, edit, and debug projects from your own terminal and IDE.


# Quickstart: Build an RPA with code
Source: https://intunedhq.com/docs/main/00-getting-started/quickstarts/rpa



In this quickstart, you'll build an RPA automation by hand—writing the code, deploying it, and triggering it via API. It's designed to walk you through the core platform concepts: Projects, APIs, Runs, and deployment.

If you'd rather skip the manual steps and have AI build your automation, see the [Intuned Agent RPA quickstart](/docs/00-getting-started/quickstarts/build-an-rpa-with-intuned-agent).

## Prerequisites

* An active Intuned account ([sign up here](https://app.intuned.io)). No credit card required—Intuned has a free plan
* Basic familiarity with TypeScript or Python

## Create and deploy your first RPA

You can develop Intuned Projects in two ways:

* **Online IDE** — Start with no local setup. Write, test, and deploy from your browser.
* **Local CLI** — Develop locally with version control and CI/CD integration.

Choose your preferred approach below.

<Tabs>
  <Tab title="Online IDE">
    <Steps>
      <Step title="Log in and create project" icon="plus">
        1. Go to [app.intuned.io/projects](https://app.intuned.io/projects) and log in.
        2. Select **Create Project**.
        3. Select your language (TypeScript or Python).
        4. Choose the **rpa-example** template.
        5. Name it `book-consultations-quickstart`.
        6. Select **Start in Intuned**.
        7. Select **Create and Open**.

        <img alt="create-rpa-project" />

        **Expected result:** The Intuned IDE opens with your project loaded.

        > **What you just got:** An **Intuned Project** groups related browser automations together. Each file in the `api/` folder becomes a callable function that controls a browser using Playwright, accepts parameters, and returns results. When you deploy this project, all its APIs go live together as a single deployable unit.
      </Step>

      <Step title="Explore the project code" icon="code">
        In the file explorer, you'll see the API file:

        **`api/book-consultations`** - A complete automation that:

        * Navigates to the booking form
        * Fills in personal details (name, email, phone)
        * Selects date, time, and consultation topic
        * Submits the form and verifies success

        **How it works:** This automation controls a browser like a human would—navigating to a website, filling form fields, clicking buttons—but executes these actions automatically based on your code and input parameters.
      </Step>

      <Step title="Run your automation in the IDE" icon="play">
        Test the automation to see it working in real-time.

        1. In the top toolbar, select **book-consultations** from the API dropdown.
        2. Select **Params #1** next to it—you'll see pre-filled test data already configured.
        3. Select the **Run** button.

           <img alt="Run RPA ide" />

        **Expected result:** The browser panel on the right shows the automation executing live. You'll see it navigate to the booking form, fill in all fields, submit, and verify success. The terminal below displays the result of the Run.

        <Expandable title="result of the Run">
          ```json theme={null}
          {
            "input": {
              "name": "Jane Smith",
              "email": "jane.smith@example.com",
              "phone": "+1(555)123-4567",
              "date": "2025-12-10",
              "time": "14:30",
              "topic": "web-scraping"
            },
            "output": {
              "success": true,
              "date": "2025-12-10",
              "message": "Consultation successfully booked for 2025-12-10 at 14:30"
            }
          }
          ```
        </Expandable>
      </Step>

      <Step title="Deploy your project" icon="rocket">
        Deploy your automation to Intuned's infrastructure.

        1. Select the **Deploy** button in the top-right corner of the IDE.
        2. In the deployment dialog, select **Deploy** to start.
        3. Watch the live deployment logs until you see "Ready".

        **Expected result:** A success message appears. Your automation is now live and ready to run.
      </Step>

      <Step title="Test in the Playground" icon="circle-check">
        Now test your deployed automation through the API Playground.

        1. In the deployment success dialog, select **Run in Playground**.
        2. In the Playground, you'll see your deployed API **book-consultations** ready to call.
        3. The request body is pre-filled with test parameters. Select **Start Run** to execute.

        <Tip>
          The Playground is just an interactive way to test your deployed automation APIs. Your automation is now callable from anywhere via API—use it from your application, service, or any HTTP client. See the [API Reference](/docs/client-apis/api-reference) for authentication and programmatic usage.
        </Tip>

        <img alt="Playground RPA run" />

        **Expected result:** The automation executes on Intuned's infrastructure. You'll see the run details and results in real-time.

        Your automation is now deployed and fully operational.
      </Step>
    </Steps>
  </Tab>

  <Tab title="Local CLI">
    <Steps>
      <Step title="Install and create project" icon="terminal">
        Open your terminal and run:

        ```bash theme={null}
            npx create-intuned-project@latest
        ```

        When prompted:

        1. **Project name:** Enter `book-consultations-quickstart`.
        2. **Language:** Select **TypeScript** or **Python**.
        3. **Template:** Choose **rpa-example**.
        4. **Your Workspace Id:** Check [here](/docs/main/03-how-to/manage/manage-workspace) how to find it.
        5. **Your API Key**: Check [here](/docs/main/03-how-to/manage/manage-api-keys) how to create one.

        **Expected result:** The CLI creates a new folder `book-consultations-quickstart/` with the project structure and installs dependencies.

        > **What you just got:** An **Intuned Project** groups related browser automations together. Each file in the `api/` folder becomes a callable function that controls a browser using Playwright, accepts parameters, and returns results. When you deploy this project, all its APIs go live together as a single deployable unit.
      </Step>

      <Step title="Navigate to project directory" icon="folder">
        ```bash theme={null}
            cd book-consultations-quickstart
        ```

        <Note>
          Dependencies are installed automatically during project creation. If they weren't, install them manually:

          * **TypeScript:** `yarn install`
          * **Python:** `uv sync`
        </Note>
      </Step>

      <Step title="Explore the project code" icon="code">
        Open the `api/` folder in your code editor. You'll see the API file:

        **`api/book-consultations`** - A complete automation that:

        * Navigates to the booking form
        * Fills in personal details (name, email, phone)
        * Selects date, time, and consultation topic
        * Submits the form and verifies success

        **How it works:** This automation controls a browser like a human would—navigating to a website, filling form fields, clicking buttons—but executes these actions automatically based on your code and input parameters.
      </Step>

      <Step title="Run your automation locally" icon="play">
        Test the automation to see it working on your machine.

        Run the following command:

        <CLICommandTabs />

        **Expected result:** The CLI launches a browser in the background. You'll see the results in your terminal.

        <Expandable title="result of the Run">
          ```json theme={null}
          {
            "input": {
              "name": "Jane Smith",
              "email": "jane.smith@example.com",
              "phone": "+1(555)123-4567",
              "date": "2025-12-10",
              "time": "14:30",
              "topic": "web-scraping"
            },
            "output": {
              "success": true,
              "date": "2025-12-10",
              "message": "Consultation successfully booked for 2025-12-10 at 14:30"
            }
          }
          ```
        </Expandable>
      </Step>

      <Step title="Deploy your project" icon="rocket">
        Deploy your automation to Intuned's infrastructure.

        Run the deployment command:

        <CLICommandTabs />

        <Note>
          If your project uses environment variables or secrets, configure them in Intuned before deploying. Local `.env` files are only used during local development and are not included in deployments. Set shared values in [workspace environment variables](/docs/main/02-features/environment-variables-secrets#workspace-level-environment-variables) or add them at the project level if they should only apply to this project.
        </Note>

        **Expected result:** You see deployment progress logs, then:

        ```bash theme={null}
        ✓ Deployment successful
        ```

        Your automation is now live and ready to run.
      </Step>

      <Step title="Test in the Playground" icon="circle-check">
        Now test your deployed automation through the API Playground.

        1. Navigate to [Playground](https://app.intuned.io/playground) in your browser.
        2. Select your project **book-consultations-quickstart** and the API **book-consultations**.
        3. The request body is pre-filled with test parameters. Select **Start Run** to execute.

        <Tip>
          The Playground is just an interactive way to test your deployed automation APIs. Your automation is now callable from anywhere via API—use it from your application, service, or any HTTP client. See the [API Reference](/docs/client-apis/api-reference) for authentication and programmatic usage.
        </Tip>

        <img alt="Playground RPA run" />

        **Expected result:** The automation executes on Intuned's infrastructure. You'll see the run details and results in real-time.

        Your automation is now deployed and fully operational.
      </Step>
    </Steps>
  </Tab>
</Tabs>

## What's next?

* **[Online IDE](/docs/main/02-features/online-ide)** — Learn more about the Intuned IDE used in this quickstart.
* **[Local development (CLI)](/docs/main/02-features/local-development-cli)** — Learn more about the Intuned CLI used in this quickstart.
* **[Authentication](/docs/main/02-features/auth-sessions)** — For automations that require login, Intuned provides built-in authentication support. You define how to log in and how to verify a session, and Intuned handles the rest—validating sessions before runs, reusing them when possible, and recreating them when expired.
* **[Monitoring and traces](/docs/main/02-features/observability-monitoring-logs)** — Every run generates detailed logs, browser traces, and session recordings. Use these tools to debug failures, verify your automation is working correctly, and understand what happened during execution.
* **[Flexible automations](/docs/main/02-features/flexible-automation)** — Build automations your way. Write deterministic code, use AI-driven extraction, or combine both in a hybrid approach. Use any library or package—Intuned is unopinionated by design.
* **[Intuned Agent quickstart](/docs/main/00-getting-started/quickstarts/intuned-agent)** — You can write your automation logic manually like in this quickstart, or use Intuned Agent to generate automations from a prompt and schema. Intuned Agent can also help you update existing automations, fix failed runs, and iterate on your code faster.
* **[Cookbook](/docs/main/01-learn/cookbook)** — Browse full working examples of automations and scrapers. Each example includes complete code you can use as a starting point for your own projects.


# Quickstart: Build a scraper with code
Source: https://intunedhq.com/docs/main/00-getting-started/quickstarts/scraper



In this quickstart, you'll build a scraper by hand—writing the code, deploying it, configuring a Job, and running it end to end. It's designed to walk you through the core platform concepts: Projects, APIs, Runs, Jobs, and deployment.

If you'd rather skip the manual steps and have AI build your scraper, see the [Intuned Agent quickstart](/docs/00-getting-started/quickstarts/intuned-agent).

## Prerequisites

* An active Intuned account ([sign up here](https://app.intuned.io)). No credit card required—Intuned has a free plan
* Basic familiarity with TypeScript or Python

## Create and deploy your first scraper

You can develop Intuned Projects in two ways:

* **Online IDE** — Start with no local setup. Write, test, and deploy from your browser.
* **Local CLI** — Develop locally with version control and CI/CD integration.

Choose your preferred approach below.

<Tabs>
  <Tab title="Online IDE">
    <Steps>
      <Step title="Log in and create project" icon="plus">
        1. Go to [app.intuned.io/projects](https://app.intuned.io/projects) and log in.
        2. Select **Create Project**.
        3. Select your language (TypeScript or Python).
        4. Choose the **e-commerce-scrapingcourse** template.
        5. Name it `ecommerce-scraper-quickstart`.
        6. Select **Start in Intuned**.
        7. Select **Create and Open**.

        <img alt="Create Project Screenshot" />

        **Expected result:** The Intuned IDE opens with your project loaded.

        > **What you just got:** An **Intuned Project** groups related browser automations together. Each file in the `api/` folder becomes a callable function that controls a browser using Playwright, accepts parameters, and returns structured results. When you deploy this project, all its APIs go live together as a single deployable unit.
      </Step>

      <Step title="Explore the project code" icon="code">
        In the file explorer, you'll see two API files:

        **`api/list`** - Navigates the e-commerce site, extracts product info from all pages, and triggers `details` for each product found.

        **`api/details`** - Visits each product page and extracts detailed information (price, SKU, descriptions, variants).

        <Tip>
          These two APIs work together—`list` discovers products and triggers `details` for each one using `extendPayload`. This pattern works well for job runs where the scope of work is determined at runtime, allowing your automation to adapt to whatever data it discovers.
        </Tip>
      </Step>

      <Step title="Run your scraper in the online IDE" icon="play">
        Test the scraper's `list` API to see it working in real-time.

        1. In the top toolbar, select **list** from the API dropdown.
        2. Select **Params #1** next to it—you'll see empty params `{}`.
        3. Select the **Run** button.

           <img alt="Run API IDE" />

        **Expected result:** The browser panel on the right shows the `list` scraper executing live. You'll see it navigate through all product pages, extract data, and paginate automatically. The terminal below what executed and the result of the Run.

        <Expandable title="result of the Run">
          ```json theme={null}
          {
            "input": {},
            "output": {
              "products": [
                {
                  "name": "Abominable Hoodie",
                  "detailsUrl": "https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie/",
                  "imageUrl": "https://www.scrapingcourse.com/ecommerce/wp-content/uploads/2024/03/mh09-blue_main.jpg"
                },
                {
                  "name": "Adrienne Trek Jacket",
                  "detailsUrl": "https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket/",
                  "imageUrl": "https://www.scrapingcourse.com/ecommerce/wp-content/uploads/2024/03/wj08-gray_main.jpg"
                },
                {
                  "name": "Aeon Capri",
                  "detailsUrl": "https://www.scrapingcourse.com/ecommerce/product/aeon-capri/",
                  "imageUrl": "https://www.scrapingcourse.com/ecommerce/wp-content/uploads/2024/03/wp07-black_main.jpg"
                }
                // ... more products
              ]
            }
          }
          ```
        </Expandable>

        **Extended payloads:** The IDE also displays a link to view the extended payloads created from this run. For each product found, you'll see a payload containing the API name `details` and the product parameters. These payloads represent additional runs that execute when running in a Job context.

        <Expandable title="extended payloads">
          ```json theme={null}
          [
              {
                  "api": "details",
                  "parameters": {
                  "name": "Abominable Hoodie",
                  "detailsUrl": "https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie/",
                  "imageUrl": "https://www.scrapingcourse.com/ecommerce/wp-content/uploads/2024/03/mh09-blue_main.jpg"
                  }
              },
              {
                  "api": "details",
                  "parameters": {
                  "name": "Adrienne Trek Jacket",
                  "detailsUrl": "https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket/",
                  "imageUrl": "https://www.scrapingcourse.com/ecommerce/wp-content/uploads/2024/03/wj08-gray_main.jpg"
                  }
              },
              // ... more extended payloads, one per product found
          ]
          ```
        </Expandable>
      </Step>

      <Step title="Deploy your project" icon="rocket">
        Deploy your scraper to Intuned's infrastructure.

        1. Select the **Deploy** button in the top-right corner of the IDE.
        2. In the deployment dialog, select **Deploy** to start.
        3. Watch the live deployment logs until you see "Ready".

        **Expected result:** A success message appears. Your scraper is now live and ready to run.
      </Step>

      <Step title="Test in the Playground" icon="circle-check">
        Now test your deployed scraper through the API Playground.

        1. In the deployment success dialog, select **Run in Playground**.
        2. In the Playground, you'll see your deployed API **list** ready to call.
        3. The request body is pre-filled with test parameters. Select **Start Run** to execute.

        <Tip>
          The Playground is just an interactive way to test your deployed automation APIs. Your scraper is now callable from anywhere via API—use it from your application, service, or any HTTP client. See the [API Reference](/docs/client-apis/api-reference) for authentication and programmatic usage.
        </Tip>

        **Expected result:** The scraper executes on Intuned's infrastructure. You'll see the run details and results in real-time.

        Your scraper is now deployed and fully operational.
      </Step>
    </Steps>
  </Tab>

  <Tab title="Local CLI">
    <Steps>
      <Step title="Install and create project" icon="terminal">
        Open your terminal and run:

        ```bash theme={null}
            npx create-intuned-project@latest
        ```

        When prompted:

        1. **Project name:** Enter `ecommerce-scraper-quickstart`.
        2. **Language:** Select **TypeScript** or **Python**.
        3. **Template:** Choose **e-commerce-scrapingcourse**.
        4. **Your Workspace Id:** Check [here](/docs/main/03-how-to/manage/manage-workspace) how to find it.
        5. **Your API Key**: Check [here](/docs/main/03-how-to/manage/manage-api-keys) how to create one.

        **Expected result:** The CLI creates a new folder `ecommerce-scraper-quickstart/` with the project structure and installs dependencies.

        > **What you just got:** An **Intuned Project** groups related browser automations together. Each file in the `api/` folder becomes a callable function that controls a browser using Playwright, accepts parameters, and returns structured results. When you deploy this project, all its APIs go live together as a single deployable unit.
      </Step>

      <Step title="Navigate to project directory" icon="folder">
        ```bash theme={null}
            cd ecommerce-scraper-quickstart
        ```

        <Note>
          Dependencies are installed automatically during project creation. If they weren't, install them manually:

          * **TypeScript:** `yarn install`
          * **Python:** `uv sync`
        </Note>
      </Step>

      <Step title="Explore the project code" icon="code">
        Open the `api/` folder in your code editor. You'll see two API files:

        **`api/list`** - Navigates the e-commerce site, extracts product info from all pages, and triggers `details` for each product found.

        **`api/details`** - Visits each product page and extracts detailed information (price, SKU, descriptions, variants).

        <Tip>
          These two APIs work together—`list` discovers products and triggers `details` for each one using `extendPayload`. This pattern works well for job runs where the scope of work is determined at runtime, allowing your automation to adapt to whatever data it discovers.
        </Tip>
      </Step>

      <Step title="Run your scraper locally" icon="play">
        Test the scraper's `list` API to see it working on your machine.

        Run the following command:

        <CLICommandTabs />

        **Expected result:** The CLI launches a browser in the background. You'll see the scraped results in your terminal.

        <Expandable title="result of the Run">
          ```json theme={null}
          {
            "input": {},
            "output": {
              "products": [
                {
                  "name": "Abominable Hoodie",
                  "detailsUrl": "https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie/",
                  "imageUrl": "https://www.scrapingcourse.com/ecommerce/wp-content/uploads/2024/03/mh09-blue_main.jpg"
                },
                {
                  "name": "Adrienne Trek Jacket",
                  "detailsUrl": "https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket/",
                  "imageUrl": "https://www.scrapingcourse.com/ecommerce/wp-content/uploads/2024/03/wj08-gray_main.jpg"
                },
                {
                  "name": "Aeon Capri",
                  "detailsUrl": "https://www.scrapingcourse.com/ecommerce/product/aeon-capri/",
                  "imageUrl": "https://www.scrapingcourse.com/ecommerce/wp-content/uploads/2024/03/wp07-black_main.jpg"
                }
                // ... more products
              ]
            }
          }
          ```
        </Expandable>

        **Extended payloads:** The CLI also displays a summary of extended payloads created from this run. For each product found, you'll see a payload containing the API name `details` and the product parameters. These payloads represent additional runs that execute when running in a Job context.

        <Expandable title="extended payloads">
          ```json theme={null}
          [
              {
                  "api": "details",
                  "parameters": {
                  "name": "Abominable Hoodie",
                  "detailsUrl": "https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie/",
                  "imageUrl": "https://www.scrapingcourse.com/ecommerce/wp-content/uploads/2024/03/mh09-blue_main.jpg"
                  }
              },
              {
                  "api": "details",
                  "parameters": {
                  "name": "Adrienne Trek Jacket",
                  "detailsUrl": "https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket/",
                  "imageUrl": "https://www.scrapingcourse.com/ecommerce/wp-content/uploads/2024/03/wj08-gray_main.jpg"
                  }
              }
              // ... more extended payloads, one per product found
          ]
          ```
        </Expandable>
      </Step>

      <Step title="Deploy your project" icon="rocket">
        Deploy your scraper to Intuned's infrastructure.

        Run the deployment command:

        <CLICommandTabs />

        <Note>
          If your project uses environment variables or secrets, configure them in Intuned before deploying. Local `.env` files are only used during local development and are not included in deployments. Set shared values in [workspace environment variables](/docs/main/02-features/environment-variables-secrets#workspace-level-environment-variables) or add them at the project level if they should only apply to this project.
        </Note>

        **Expected result:** You see deployment progress logs, then:

        ```bash theme={null}
        ✓ Deployment successful
        ```

        Your scraper is now live and ready to run.
      </Step>

      <Step title="Test in the Playground" icon="circle-check">
        Now test your deployed scraper through the API Playground.

        1. Navigate to [Playground](https://app.intuned.io/playground) in your browser.
        2. Select your project **ecommerce-scraper-quickstart** and the API **list**.
        3. The request body is pre-filled with test parameters. Select **Start Run** to execute.

        <Tip>
          The Playground is just an interactive way to test your deployed automation APIs. Your scraper is now callable from anywhere via API—use it from your application, service, or any HTTP client. See the [API Reference](/docs/client-apis/api-reference) for authentication and programmatic usage.
        </Tip>

        **Expected result:** The scraper executes on Intuned's infrastructure. You'll see the run details and results in real-time.

        Your scraper is now deployed and fully operational.
      </Step>
    </Steps>
  </Tab>
</Tabs>

## What's next?

* **[Jobs](/docs/main/02-features/jobs-batched-executions)** — Jobs are the common way to run scrapers. Configure a schedule (daily, hourly, or custom) and define a sink to send your scraper results to a webhook, S3 bucket, or other destination.
* **[Authentication](/docs/main/02-features/auth-sessions)** — For scrapers that require login, Intuned provides built-in authentication support. You define how to log in and how to verify a session, and Intuned handles the rest—validating sessions before runs, reusing them when possible, and recreating them when expired.
* **[Monitoring and traces](/docs/main/02-features/observability-monitoring-logs)** — Every run generates detailed logs, browser traces, and session recordings. Use these tools to debug failures, verify your scraper is working correctly, and understand what happened during execution.
* **[Flexible automations](/docs/main/02-features/flexible-automation)** — Build scrapers your way. Write deterministic code, use AI-driven extraction, or combine both in a hybrid approach. Use any library or package—Intuned is unopinionated by design.
* **[Intuned Agent quickstart](/docs/main/00-getting-started/quickstarts/intuned-agent)** — You can write your scraper logic manually like in this quickstart, or use Intuned Agent to build it from a prompt. Intuned Agent can also help you update existing projects, fix failed runs, and iterate on your code faster.
* **[Cookbook](/docs/main/01-learn/cookbook)** — Browse full working examples of scrapers and other automations. Each example includes complete code you can use as a starting point for your own projects.
* **[Online IDE](/docs/main/02-features/online-ide)** — Learn more about the Intuned IDE used in this quickstart.
* **[Local development (CLI)](/docs/main/02-features/local-development-cli)** — Learn more about the Intuned CLI used in this quickstart.


# Cookbook (GitHub)
Source: https://intunedhq.com/docs/main/01-learn/cookbook





# Intuned in depth
Source: https://intunedhq.com/docs/main/01-learn/deep-dives/intuned-indepth

Explore how Intuned works - the long version

## Introduction

<Tip>
  Looking for a quick overview? Check out [How the platform works](/docs/main/00-getting-started/how-intuned-works) for the short version.
</Tip>

Intuned is a platform that enables developers to build and consume browser automations as code. Turn complex browser interactions into reliable, scalable APIs.

Intuned allows you to:

* **Organize** your automations into projects
* **Develop** these automations as APIs
* **Deploy** them effortlessly with no setup required
* **Execute** them reliably on demand or at a schedule at any scale
* **Monitor** their execution
* **Automate the hard parts** with Intuned Agent—an autonomous agent that builds and maintains browser automations for you

## Projects

A **Project** is both your automation code and your runtime environment.

A project usually represents a browser automation with a specific goal, such as:

* A single website scraper
* An integration with a platform

### As code

A Project contains your browser automation APIs and shared logic—written in <Icon icon="https://d3gk2c5xim1je2.cloudfront.net/devicon/python.svg" /> **Python** or <Icon icon="https://d3gk2c5xim1je2.cloudfront.net/devicon/typescript.svg" /> **TypeScript** using Playwright.

```
Project/
├── apis/
│   ├── scrape-product.ts    # Your automation APIs
│   └── submit-form.ts
├── helpers/                  # Shared utilities
└── Intuned.json              # Project configuration
```

Each **API** is a function that receives a browser page and parameters, performs actions, and returns results. You have full control over the code—use any library, pattern, or approach that fits your needs.

### At runtime

A Project is where everything comes together: Run records, Jobs, JobRuns, browser traces, logs, and AuthSessions all live within the Project that owns them.

When you execute an API in a project, it produces a Run record associated with that project. The same applies to Jobs. See [Execute](#execute) for details.

## Develop

You can develop any project in the [online IDE](/docs/main/02-features/online-ide),
and with the [local CLI](/docs/main/02-features/local-development-cli).

A project's code is divided into APIs that fall under the `apis` directory in your project's code, with file system based routing. Each API is a function that accepts a browser page (via Playwright), takes parameters to customize the execution, and returns results.

```
apis/
  ├── scrape-product-list.ts
  ├── scrape-product-details.ts
  ├── order-product.ts
  └── ...
```

[Intuned Agent](#intuned-agent) can help you build and maintain your project code.

## Deploy

When you deploy a project, the code, dependencies, and configurations are built, packaged, and deployed on dedicated virtual machines.

### Deployment model

Intuned uses different deployment strategies for Standalone Runs and Jobs to optimize for their distinct execution patterns.

#### Standalone APIs

Intuned deploys a pool of machines dedicated to handling your Standalone Run requests.

This pool is controlled by the `minCount` and `maxCount` settings in your project's replication settings. Intuned keeps `minCount` machines always ready, and scales the pool up to `maxCount` under load. Each machine handles one request at a time—this ensures full isolation and consistent performance for each Run.

When more requests arrive than there are available machines:

1. The pool scales up, adding machines up to `maxCount`
2. Once the pool is at `maxCount` and every machine is busy, incoming requests are queued
3. As machines complete their current Run, they pull the next request from the queue in order

This model provides predictable scaling: if you set `maxCount` to 5, your project can process up to 5 Runs concurrently.

#### Jobs (JobRuns)

Jobs use a different deployment model optimized for batch execution. Instead of a persistent pool, each JobRun provisions its own dedicated machines based on the **machineCount** setting defined in [`Intuned.json`](/docs/main/05-references/intuned-json). See [Jobs (batched execution)](/docs/main/02-features/jobs-batched-executions) for configuration details.

When a JobRun starts:

1. Intuned provisions the specified number of machines for that JobRun
2. The Runs within the JobRun are distributed across these machines
3. Once the JobRun completes, the machines are removed

This approach ensures Jobs get dedicated resources for their execution without affecting the Standalone API pool, and allows you to scale each JobRun independently based on its workload.

## Execute

Once your project is deployed, you can start executing the APIs you developed.

### Runs and Attempts

Every execution creates records that track what happened.

#### Run

A **Run** represents the logical execution of a single API. Each Run has one or more Attempts. A Run takes as input:

* **API name** that you want to execute.
* **Parameters** that you provide. These parameters are passed to the API function to customize its execution.
* **Configuration** options like attempt limits, timeouts, authentication, and proxy settings

Once you trigger a Run, it enters a **pending** state while waiting to be picked up for execution. When picked up, it changes to **started** and remains there until execution completes, then transitions to **success** or **failed**. While **pending** or **started**, a Run may be **canceled** manually or automatically if pre-execution dependencies fail (e.g., authentication failure).

Each Run produces either:

* **Result** data returned by the API if the Run is successful
* **Error** information if the Run fails or is canceled.

#### Attempt

An **Attempt** is the actual execution of the API code. While a Run is the logical execution, an Attempt is where the code actually runs. A Run always has at least one Attempt. If an Attempt fails, the Run creates a new Attempt (a retry) until the API succeeds or the maximum number of Attempts is reached. Each Run has at most one successful Attempt.

Before an Attempt runs, it may trigger dependency Runs, such as [authentication validation](#authsessions). Currently, `AuthSession:Validate` is the only dependency Run type. If these pre-execution dependencies fail, the Attempt will be canceled, causing its Run to also be canceled.

Each Attempt produces its own:

* **Result** data returned by the API if the Attempt is successful.
* **Error** information if the Attempt fails or is canceled.
* **[Playwright Traces](/docs/main/02-features/observability-monitoring-logs)** that show a detailed trace of the browser interactions during the Attempt execution, which can be useful for debugging and monitoring.
* **Logs** that provide insights into the Attempt execution flow, useful for debugging and monitoring.

The result or error of a Run is the result or error of its last Attempt.

This architecture provides fine-grained control and visibility while logically grouping related API executions, so you can manage and trace complex automation workflows.

### Ways to trigger executions

There are two ways to trigger API executions: standalone Runs for on-demand execution, and Jobs for batch or scheduled execution.

#### Standalone Runs (single executions)

Standalone Runs allow you to execute a single API on demand.

A standalone Run allows you to configure:

* **Single API** and its **parameters**
* **Run configurations** like attempt limits, timeouts, authentication, and proxy settings
* Optional **sink** to send results to (e.g., webhook, blob storage, etc...)

Single Runs are perfect for:

* On-demand executions (e.g., RPA tasks)
* Testing and debugging

See [Runs (single executions)](/docs/main/02-features/runs-single-executions) for details.

#### Jobs (batched executions)

Jobs are a way to trigger or schedule multiple correlated Runs, track their progress, and monitor their results together.

Jobs allow you to configure:

* **Multiple APIs**, each with its own **parameters**.
* **Run configurations** like attempt limits, timeouts, authentication, and proxy settings that apply to all Runs.
* **Concurrency settings** to control how many Runs can execute in parallel.
* Optional **scheduling options** (e.g., run every hour, daily, etc.)
* Optional **sink** to send results to (e.g., webhook, blob storage, etc...)

Jobs are blueprints that define how multiple APIs execute together. Once executed, a Job creates a JobRun—a collection of Runs triggered as part of that execution. Jobs also support nested scheduling, where one API can dynamically expand the JobRun's workload. For more details, check out the [nested scheduling section](#nested-scheduling).

Jobs are perfect for:

* Running multiple APIs in bulk and aggregating their results (e.g., web scraping)
* Running automations on a schedule

See [Jobs (batched execution)](/docs/main/02-features/jobs-batched-executions) for details.

### Nested scheduling

Nested scheduling is a feature that allows APIs that are running as part of a JobRun to extend their execution with other APIs. When an API is executed successfully, the APIs it extends will also be executed as part of the same JobRun.

An API can extend the JobRun's payload using the [extend payload](/docs/main/05-references/runtime-sdk-typescript/extend-payload) function part of [Intuned's Runtime SDK](/docs/main/05-references/runtime-sdk-typescript/overview).

**Common use case: dynamic scraping**
Let's say you're scraping an e-commerce site where the product pages change frequently, and you don't know all the product pages you need to scrape in advance. You can use nested scheduling to handle this dynamic workload:

* Initial API: finds all product pages that need to be scraped
* Uses [`extendPayload`](/docs/main/05-references/runtime-sdk-typescript/extend-payload) to expand the payload of the JobRun
* Extended APIs: Runs and extracts each product details from product pages

This pattern is essential when the automation payload can change across JobRuns.

<Accordion title="Example flow">
  ```mermaid theme={null}
  graph TD
   A[JobRun starts with one API: list] --> B[list API executes]
   B --> C[list calls extendPayloads]
   C --> D[Adds 5 details APIs to JobRun]
   D --> E[list API completes successfully]
   E --> F[JobRun continues with extended APIs]
   F --> G[Execute details API 1]
   F --> H[Execute details API 2]
   F --> I[Execute details API 3]
   F --> J[Execute details API 4]
   F --> K[Execute details API 5]
   G --> L[JobRun completes]
   H --> L
   I --> L
   J --> L
   K --> L
   L --> M[Final result: list + 5 details APIs]
  ```
</Accordion>

## Monitor

Every Run and Attempt produces detailed records you can inspect—statuses, results, logs, and browser traces to help you debug and optimize your automations.

Each Run record includes:

* **Current status** (Pending, Started, Success, Failed, Canceled)
* **Start and end timestamps**
* **Parameters** and **configuration** used
* **Result** data if successful
* **Error** information if failed or canceled
* Link to **Job** and **JobRun** (if applicable)
* Link to **AuthSession** (if applicable)
* **Attempts** associated with the Run.
* Links to **[pre-execution dependency runs](#authsessions)** (if applicable)

Each Attempt record includes:

* **Start and end timestamps**
* **Parameters** and **configuration** used
* **Result** data if successful
* **Error** information if failed or canceled
* **Playwright Trace** link for detailed browser interaction trace
* **Logs** for debugging and monitoring

You can view these records from the [Runs](/docs/main/02-features/runs-single-executions) section in your project on the Intuned Dashboard.

If you're using Jobs, each JobRun creates a record that includes:

* **Current status** (In Progress, Completed, Failed)
* **Start and end timestamps**
* List of **Runs** associated with the JobRun

You can view these records from the [Jobs](/docs/main/02-features/jobs-batched-executions) section in your project on the Intuned Dashboard.

See [Monitoring and traces](/docs/main/02-features/observability-monitoring-logs) for details.

## AuthSessions

Many browser automations require login to access content or perform actions:

* Automating internal tools
* Interacting with applications that have no APIs
* Automating back-office workflows on behalf of users
* Scraping user-specific data

Authentication can often be tricky to handle, especially when running at scale. When should I log in and when should I reuse existing sessions? How do I know that my session is still valid? What if I'm being logged out frequently?

**[AuthSessions](/docs/main/02-features/auth-sessions)** are Intuned's answer to these challenges: a set of features to streamline authentication in browser automation.

### How to use AuthSessions

Once you enable AuthSessions for your project (authed project):

* All Runs will require an AuthSession to be executed.
* Before each Attempt, the AuthSession will be validated and recreated if needed.
* AuthSession creation and validation is defined in code.
  * [`auth-sessions/check` and `auth-sessions/create`](/docs/main/02-features/auth-sessions) APIs in your project.

This ensures every API runs with a valid session and keeps authentication consistent across all project APIs.

When you trigger a Run for an Authed Project, you must provide an AuthSession as part of the Run configuration. You can use different AuthSessions for different Runs, allowing you to run automations on behalf of different users, and reuse AuthSessions across multiple Runs.

### AuthSession usage patterns

* **Single account automation:**
  * **Example:** Company admin dashboard. Create one AuthSession to scrape analytics data, export reports, and update settings—each as its own API. All API executions can share the same AuthSession.
  * **Example:** Authenticated data scraper. Create one AuthSession to log into a website and scrape available data.
* **Multi-account automation:** User-specific integrations.
  * **Example:** Social media manager tool. Create multiple AuthSessions (one per user when they "connect") and run posting/scheduling APIs on behalf of each user. User A has AuthSession A, User B has AuthSession B.

### AuthSession Runs

AuthSessions come with special types of Runs that encapsulate authentication operations. Unlike regular API Runs, these can combine different API Attempts in a single Run:

#### `AuthSession:Validate`

* Triggered before each authed project API Attempt.
* Its goal is to ensure a valid AuthSession is ready for the API Attempt to execute. More on this in the next section.
* Starts with one or more `check` Attempts that run the `check` API.
* If validation fails and **auto recreation** is enabled (which is true by default), it will execute `create` Attempts to recreate the AuthSession, and then reexecute `check` Attempts to re-validate it.

#### `AuthSession:Create`

* Triggered when you manually create an AuthSession via dashboard or API.
* Starts with one or more `create` Attempts to capture the browser state then `check` Attempts to validate it.

#### `AuthSession:Update`

* Triggered when you manually update an existing AuthSession via dashboard or API.
* Starts with one or more `create` Attempts to recreate the AuthSession then `check` Attempts to validate it.

### How AuthSessions work

In authed projects, each Attempt execution will first validate the provided AuthSession by running a dependency Run (`AuthSession:Validate`), which ensures your API code never runs with an invalid session.

If the `AuthSession:Validate` fails, the Attempt is canceled, which in turn cancels the entire Run without retrying. This is different from failed Attempts due to API execution errors, which will retry until the maximum number of Attempts is reached.

```mermaid theme={null}
flowchart TD
  Start((Start)) --> Attempt[Current Attempt = 1]
  Attempt --> Validate[AuthSession: Validate Run]

  
  Validate --> Valid{Valid?}
  Valid -->|No| CancelAttempt[Cancel Attempt]
  Valid -->|Yes| ExecuteAttempt[Execute Attempt]
  

  ExecuteAttempt -->|Success| RunSuccessful[Run Successful]

  ExecuteAttempt -->|Failed| CheckFinal{Final Attempt?}
  CheckFinal -->|Yes| RunFailed[Run Failed]
  CheckFinal -->|No| IncrementAttempt[Attempt += 1] 
  IncrementAttempt --> Validate
  
  
  CancelAttempt --> CancelRun[Cancel Run]
  
  RunSuccessful --> EmptyCircle(( ))
  RunFailed --> EmptyCircle(( ))
  CancelRun --> EmptyCircle(( ))

  
  style RunSuccessful stroke:#0bd02c,stroke-width:3px
  style RunFailed stroke:#ec1313,stroke-width:3px
  style CancelRun stroke:#ffaa00,stroke-width:3px
  style EmptyCircle stroke-width:3px
```

`AuthSession:Validate` also supports **auto recreation**, which is enabled by default. When enabled, if the AuthSession check fails, Intuned will automatically attempt to recreate the AuthSession (by running the create code), and then re-validate it. If the recreation and re-validation succeed, the Attempt continues to execute the API. If either the recreation or re-validation fails, the validation run is considered failed and the Attempt is canceled.

```mermaid theme={null}
---
title: AuthSession:Validate
---
flowchart TD
  Start((Start)) --> CheckValid[Run Check API with retries]
  
  CheckValid -->|Passed| ValidationSuccess[Validation Success]
  CheckValid -->|Failed| AutoRecreate[Auto Recreate Enabled?]
  
  AutoRecreate{Auto Recreate?} -->|Yes| Recreate[Run Create API with retries]
  AutoRecreate -->|No| ValidationFailed[Validation Failed]
  
  Recreate -->|Success| CheckValidAgain[Run Check API again with retries]
  Recreate -->|Failed| ValidationFailed[Validation Failed]
  
  CheckValidAgain -->|Passed| ValidationSuccess[Validation Success]
  CheckValidAgain -->|Failed| ValidationFailed
  
  style ValidationSuccess stroke:#0bd02c,stroke-width:3px
  style ValidationFailed stroke:#ec1313,stroke-width:3px
```

**Key takeaways:**

1. **Every Attempt validates the AuthSession before execution** by triggering an AuthSession validation Run as a pre-execution dependency.
2. **Failed validation cancels the Attempt** and therefore the Run.
3. **Auto recreation can be controlled per-Run**.

### AuthSession types

Intuned supports three patterns for creating and managing AuthSessions:

| Type                  | Who authenticates | Auto-recreation | Credentials stored | Best for                                 |
| --------------------- | ----------------- | --------------- | ------------------ | ---------------------------------------- |
| **Credentials-based** | Code              | ✓               | By Intuned         | Standard login flows                     |
| **Runtime-based**     | Code              | Required        | Not stored         | Changing credentials (OTPs, temp tokens) |
| **Recorder-based**    | Human             | ✗               | —                  | Complex login (MFA, CAPTCHA, biometrics) |

**Credentials-based** AuthSessions use stored credentials to log in and create sessions. You create an AuthSession instance via dashboard or API, and Intuned triggers an AuthSession:Create Run that creates and validates the AuthSession (using the `create` and `check` APIs) with the provided credentials, then captures the browser state after completion. The credentials are stored securely by Intuned and used for recreation when the browser state expires or becomes invalid.

This approach is suitable for programmatic login flows (e.g., username/password, API keys).

To use this AuthSession, you provide its ID in the Run configuration.

```jsonc theme={null}
{
  "api": "scrape-dashboard",
  "parameters": { /* ... */ },
  "authSession": {
    "id": "auth-session-1"
  }
}
```

When you trigger an authed API Run, Intuned reads the stored browser state and validates it before the API Attempt execution. If validation fails and auto recreate is enabled, Intuned will read the stored credentials, run the `create` API to recreate the session, and then re-validate it before proceeding with the Attempt execution. This all happens as part of the `AuthSession:Validate` run as mentioned above.

<Accordion title="Figure: Attempt flow with credentials-based AuthSession">
  ```mermaid theme={null}
  flowchart TD
    Start((Start)) --> ValidationProcess
    
    
    subgraph ValidationProcess[AuthSession Validate]

      ReadState[Read Browser State from Intuned] --> Validate[Check AuthSession]
      Validate --> Valid{Valid?}
      Valid -->|No| AutoRecreate{Auto Recreate On?}
      AutoRecreate -->|Yes| ReadCreds[Read Credentials from Intuned]
      ReadCreds --> Recreate[Recreate AuthSession]
      Recreate --> RecreateSuccess{Recreate Success?}
      RecreateSuccess -->|Yes| SaveNew[Save New Browser State]
    end
    AutoRecreate -->|No| Cancel[Cancel Attempt]
    RecreateSuccess -->|No| Cancel
    
    SaveNew --> Execute[Execute Attempt]
    Valid -->|Yes| Execute
    
    style Execute stroke-width:3px
    style Cancel stroke:#ffaa00,stroke-width:3px
  ```
</Accordion>

**Runtime-based** AuthSessions are created when you provide credentials as part of the Standalone Run API request—not as a separate `AuthSession:Create` Run from the dashboard or API. Runtime-based AuthSessions always have auto recreation enabled.

This approach is suitable when credentials change frequently or cannot be stored (e.g., OTPs, temporary tokens).

To use this AuthSession, you provide its ID and credentials in the Run configuration. If an ID is not provided, each Run will create a brand new AuthSession.

```jsonc theme={null}
{
  "api": "scrape-dashboard",
  "parameters": { /* ... */ },
  "authSession": {
    "id": "auth-session-1",
    "runtimeInput": { /* ... */}
  }
}
```

When you trigger a run, before Attempt execution, if an AuthSession ID is provided and it exists, Intuned reads the stored browser state to validate against it. Otherwise, Intuned validates against an empty state. If validation fails, Intuned will use the passed credentials to run the `create` API to recreate the AuthSession, and then re-validate it before proceeding with the API Attempt execution.

<Accordion title="Figure: Attempt flow with runtime-based AuthSession">
  ```mermaid theme={null}
  flowchart TD
    Start((Start)) --> ValidationProcess
    
    subgraph ValidationProcess[AuthSession Validate]
    
      CheckExist{AuthSession Exists?} -->|Yes| ReadState[Read Browser State from Intuned]
      CheckExist -->|No| CreateNew[Create New AuthSession]
      CreateNew --> EmptyState[Use Empty Browser State]
      
      ReadState --> Validate[Check AuthSession]
      EmptyState --> Validate

      Validate --> Valid{Valid?}
      Valid -->|No| ReadCreds[Use Credentials from Run Config]
      ReadCreds --> Recreate[Recreate AuthSession]
      Recreate --> RecreateSuccess{Recreate Success?}
      RecreateSuccess -->|Yes| SaveNew[Save New Browser State]
    end

    RecreateSuccess -->|No| Cancel[Cancel Attempt]
    SaveNew --> Execute[Execute Attempt]
    Valid -->|Yes| Execute
    style Execute stroke-width:3px
    style Cancel stroke:#ffaa00,stroke-width:3px
  ```
</Accordion>

**Recorder-based** AuthSessions allow you (or your users) to manually log in to a website using a browser recorder tool (hosted cloud-based browser). You perform the login steps in a real browser, and the recorder captures the browser state after a successful login. This captured state is then used for subsequent Runs.

<Note>If you need more information about how recorder-based AuthSessions work and how to enable and use them, contact support via Slack channel or [email](mailto:support@intunedhq.com).</Note>

### AuthSessions with Jobs

Jobs fully support AuthSessions. When you configure a Job for an authed Project, you specify the AuthSession in the job configuration. Each Run triggered as part of the JobRun will use the specified AuthSession, including extended Runs (via [`extendPayload`](/docs/main/05-references/runtime-sdk-typescript/extend-payload) [nested scheduling](#nested-scheduling)). Auto recreation is configured at the Job level.

Jobs have special handling for AuthSessions to ensure efficient execution at scale:

* When a JobRun starts, it first validates the AuthSession by triggering an `AuthSession:Validate` Run, which can recreate the AuthSession if needed.
* If the `AuthSession:Validate` Run fails, the JobRun is canceled.
* Each Run in the JobRun will also validate the AuthSession before each Attempt execution.
* If any `AuthSession:Validate` fails and recreation attempts fail, the JobRun pauses. You can then manually recreate the AuthSession via dashboard or API, and then resume the JobRun.

<Note>
  Each Attempt in the JobRun executes `AuthSession:Validate` with auto recreation disabled. If validation fails, the Attempt and Run are canceled. The JobRun then reruns `AuthSession:Validate` with auto-recreate enabled (the JobRun controls recreation, not individual Attempts). If this succeeds, the JobRun reschedules the canceled Runs with the new AuthSession state. This ensures efficiency, reliability, and prevents race conditions.
</Note>

<Accordion title="Figure: JobRun flow with AuthSessions">
  <Tabs>
    <Tab title="auto-recreate=true (default)">
      ```mermaid theme={null}
      flowchart TD
        Start((Start)) --> InitialValidate[AuthSession:Validate auto-recreate=true]

        InitialValidate --> AuthSessionValid{Valid?}
        AuthSessionValid -->|No| Canceled[JobRun Canceled]
        AuthSessionValid -->|Yes| ExecuteRuns[Execute Runs - each Attempt validates and auto-recreate=false]


        ExecuteRuns -->|All Runs Complete| Completed[JobRun Completed]
        ExecuteRuns -->|AuthSession Validation Failed on Attempt| InProgressValidate[AuthSession Validate with auto-recreate=true]
        InProgressValidate --> InProgressValid{Valid?}
        InProgressValid -->|Yes| RescheduleRuns[Reschedule Canceled Runs]
        RescheduleRuns --> ExecuteRuns
        InProgressValid -->|No| Paused[JobRun Paused]

        Canceled --> EmptyCircle(( ))
        Completed --> EmptyCircle
        Paused --> EmptyCircle
        
        style Canceled stroke:#ffaa00,stroke-width:3px
        style Completed stroke:#0bd02c,stroke-width:3px
        style Paused stroke:#ffaa00,stroke-width:3px
      ```
    </Tab>

    <Tab title="auto-recreate=false">
      ```mermaid theme={null}
      flowchart TD
        Start((Start)) --> InitialValidate[AuthSession:Validate auto-recreate=false]

        InitialValidate --> AuthSessionValid{Valid?}
        AuthSessionValid -->|No| Canceled[JobRun Canceled]
        AuthSessionValid -->|Yes| ExecuteRuns[Execute Runs - each Attempt validates]


        ExecuteRuns -->|All Runs Complete| Completed[JobRun Completed]
        ExecuteRuns -->|AuthSession Validation Failed on Attempt| InProgressValidate[AuthSession Validate]
        InProgressValidate --> InProgressValid{Valid?}
        InProgressValid -->|Yes| RescheduleRuns[Reschedule Canceled Runs]
        RescheduleRuns --> ExecuteRuns
        InProgressValid -->|No| Paused[JobRun Paused]

        Canceled --> EmptyCircle(( ))
        Completed --> EmptyCircle
        Paused --> EmptyCircle
        
        style Canceled stroke:#ffaa00,stroke-width:3px
        style Completed stroke:#0bd02c,stroke-width:3px
        style Paused stroke:#ffaa00,stroke-width:3px
      ```
    </Tab>
  </Tabs>
</Accordion>

### Proxy support

You can configure a proxy to use with AuthSessions. When set, all runs using that AuthSession (Create, Update, Validate, and API Runs) use the specified proxy for browser interactions. This ensures all authentication and API executions route through the desired proxy. To change the proxy, update the AuthSession configuration.

## Key features

### Proxies

Websites may block traffic based on IP addresses, especially if they detect a high volume of requests from a single IP. Intuned allows you to configure proxies on Runs, enabling you to route your traffic through different IP addresses and avoid IP-based blocking.

See [Proxies](/docs/main/02-features/stealth-mode-captcha-solving-proxies#proxies) for details.

### Stealth mode

Automation libraries like Playwright often leave detectable fingerprints that websites can use to identify and block automated traffic. Intuned's stealth mode helps your automations avoid detection by masking these fingerprints, making your browser interactions appear more like those of a real user. This can help in most bot-detection scenarios. See [Stealth mode](/docs/main/02-features/stealth-mode-captcha-solving-proxies#stealth-mode) for details.

### CAPTCHA solving

Some websites use advanced anti-bot measures, such as CAPTCHAs, to verify that a user is human. Intuned provides built-in support for solving CAPTCHAs encountered during browser automation, allowing your automations to continue running smoothly even when faced with these challenges. The CAPTCHA solver requires a headful browser. See [CAPTCHA solving](/docs/main/02-features/stealth-mode-captcha-solving-proxies#captcha-solving) for details.

### Intuned Agent

[Intuned Agent](/docs/main/02-intuned-agent/overview) is an AI agent that builds and maintains browser automations. It can:

* Create projects from a prompt—scrapers, RPA workflows, crawlers, and AI-powered automations.
* Edit existing projects to add features, change logic, or update configuration.
* Fix broken automations by analyzing failed Runs and applying code changes.
* [Maintain projects automatically](/docs/main/02-intuned-agent/self-healing-projects)—detecting production issues and fixing them without manual intervention.

See [Intuned Agent](/docs/main/02-intuned-agent/overview) for details.

### Observability

Intuned provides full observability into every Run, Attempt, and JobRun, giving you complete visibility into what happened and why. Each execution generates detailed logs, browser traces, and session recordings that help you debug issues, optimize performance, and understand your automation's behavior.

These tools make it easy to diagnose failures, verify that your automations are working correctly, and identify areas for improvement.

See [Observability](/docs/main/02-features/observability-monitoring-logs) for details.

## What's next?

* **[Intuned vs. Others](/docs/main/01-learn/deep-dives/intuned-vs-others)** — How does Intuned compare to other tools and platforms
* **[Playwright for Automation](/docs/main/01-learn/deep-dives/playwright)** — Deep dive into Playwright and how to use it for browser automation
* **[Self-healing projects](/docs/main/02-intuned-agent/self-healing-projects)** — Learn how Intuned Agent detects issues and fixes projects automatically


# Intuned vs other approaches
Source: https://intunedhq.com/docs/main/01-learn/deep-dives/intuned-vs-others

Compare browser automation approaches and understand where Intuned fits

We frequently get asked: "How is Intuned different from X?" This document answers those questions.

This document has two sections. First, [Intuned as a platform](#intuned-as-a-platform) covers how we compare to other browser automation solutions. Then [Intuned Agent](#intuned-agent) covers how our AI agent that builds and maintains browser automations compares to others.

## Intuned as a platform

This section covers how Intuned as a platform compares to other browser automation solutions—and in some cases, how they fit together.

### Browser-as-a-service vs. Intuned

**Examples:** Browserbase, Browserless

These services expose managed browser instances via CDP (Chrome DevTools Protocol). You write automation code that runs on your own infrastructure and connects to their browsers remotely.

With Intuned, you deploy a Project—a codebase containing your automation logic split into APIs. Your code runs alongside a browser with built-in stealth mode and captcha solving. You call those APIs to execute automations, and the platform handles everything else: scheduling, retries, authentication, and result delivery.

#### Key differences

**Performance.** With browser-as-a-service, CDP traffic travels over the network. CDP wasn't designed for this. Playwright generates heavy round trips for auto-wait, element selectors, and assertions. What runs in milliseconds locally can take seconds remotely. Moving assets like screenshots, downloads, and images adds more overhead. With Intuned, your code and browser share the same machine. No network latency, no slow asset transfers.

**What's included.** Browser-as-a-service gives you a browser, but you still need to run your code somewhere, handle scheduling, manage authentication, and store results. With Intuned, everything lives in one platform—one deployment, one cost.

**Observability.** Browser-as-a-service gives you browser recordings. Intuned gives you that plus structured tracing tied to your automation logic—every Run tracks API parameters, outputs, timing, and browser traces together. You debug at the automation level, not just the browser level.

<Tip>
  Learn more about [Observability, monitoring, and logs](/docs/main/02-features/observability-monitoring-logs).
</Tip>

### AI automation libraries vs. Intuned

**Examples:** Skyvern, Browser Use, Stagehand

These range from open-source libraries you integrate into your code (Browser Use, Stagehand) to managed services (Skyvern, Browser Use). They share one thing: AI controls the browser at runtime. Instead of writing selectors and navigation logic, you describe what you want and the AI figures out how to do it.

Intuned provides the infrastructure to run any browser automation. You can use [Browser Use](/docs/main/04-integrations/browser-use) or [Stagehand](/docs/main/04-integrations/stagehand) on Intuned, write Playwright code, or [mix approaches in the same project](/docs/main/02-features/flexible-automation).

For one-off tasks, AI-driven automation can work well. For repetitive automation—scrapers that run daily, browser-based RPA workflows that execute thousands of times—deterministic code is more reliable and cost-effective. [Intuned Agent](/docs/main/02-intuned-agent/overview) can help you write and maintain that code, but AI doesn't need to run at execution time.

#### Why deterministic code for repetitive tasks

Here's what makes AI-driven automation less suited for repetitive work:

**Cost.** Many actions require LLM calls. While some libraries offer caching, it's limited and not effective in many cases. For a scraper that runs daily on thousands of pages, costs add up fast. Deterministic code has no per-action overhead.

**Speed.** AI solutions rebuild the HTML tree after each action to check if the goal is reached. Even simple tasks take longer than deterministic code.

**Consistency.** The same prompt can produce different behavior across runs. When you need automation that works the same way every time, unpredictability is a problem.

**Debuggability.** With deterministic code, you know exactly what ran and why. With AI-driven code, decisions are made by the LLM—when something fails, it's harder to understand what happened or reproduce the issue.

**Control.** AI agents typically expose 8–10 high-level actions: click, type, scroll, select, upload file, hover, press key. Sometimes you need more:

* **Network control:** Intercept requests, block resources, capture API responses, mock data
* **JavaScript execution:** Query the DOM directly, access JS variables, trigger custom events
* **Timing and synchronization:** Wait for specific conditions, network idle, or race multiple events
* **State management:** Manipulate cookies, localStorage, sessionStorage
* **File operations:** Handle downloads to specific paths, process files mid-automation
* **Error handling:** Custom retry strategies, conditional branching on failures
* **Performance optimization:** Block images, fonts, or analytics to speed up automation
* **Non-standard elements:** Custom HTML components don't work with pre-written AI skills

### API-based web extraction vs. Intuned

**Examples:** Firecrawl

API-based solutions provide web extraction as APIs (crawl, scrape, etc.). You call the API, you get data extracted from one or more websites. These solutions focus on scraping and extracting data.

With Intuned, you deploy a Project—a codebase containing any logic split into APIs. You execute your code via Runs or Jobs, triggered through the UI/API. You control how the execution works via code, what libraries to use, and how to handle edge cases.

#### Key differences

**Ease of use vs. control.** API-based solutions give you a ready-to-use API. No need to understand how browser automation works, what libraries to use, or how to write extraction logic. You just call the API and get results. The trade-off is less control—you can't customize how things work under the hood. With Intuned, you write code that handles any situation that fits your use case.

**Flexibility.** With Intuned, you decide how extraction works. Want to crawl only specific types of pages based on custom logic? You control the crawl logic. Want to use a specific model or prompt for AI extraction? You choose. Want to use a particular library or pattern for extracting markdown? It's your code. API-based solutions don't offer this level of customization.

**Authentication.** API-based solutions typically don't handle login flows or session management. Intuned has built-in AuthSession support for authenticated extractions.

**Pricing.** API-based solutions charge per API call or per page. For large-scale extraction, costs grow linearly with volume. Intuned charges for runtime—the time your automation runs. This tends to be more cost-effective at scale.

#### When API-based extraction makes sense

API-based extraction works well when you don't want to deal with the details of how extraction works and you don't need customization or flexibility. It's a good fit for simple, public pages where you need quick results without writing code.

As usage grows and you start targeting specific types of websites, customization and flexibility matter more. That's when companies tend to build their own solutions—more customized, more cost-effective, and tailored to their needs. That's where Intuned fits.

<Tip>
  Intuned has a [starter project](https://github.com/Intuned/cookbook/tree/main/python-examples/firecrawl) with scrape, crawl, and other APIs similar to Firecrawl. You can use it directly or customize it.
</Tip>

### Computer use models vs. Intuned

**Examples:** Claude Computer Use, OpenAI Operator, Gemini Computer Use

These models from OpenAI, Anthropic, Google, and other labs let you control a browser or entire desktop through AI. They take screenshots, reason about what they see, and issue mouse and keyboard commands.

These labs provide model access, but you still need infrastructure to run the browser, manage execution, handle retries, and store results. Intuned provides that infrastructure. You can use computer use models on Intuned if they fit your use case, just like you can use Playwright, [Browser Use](/docs/main/04-integrations/browser-use), or [Stagehand](/docs/main/04-integrations/stagehand).

<Tip>
  See our computer use examples in [Python](https://github.com/Intuned/cookbook/tree/main/python-examples/computer-use) and [TypeScript](https://github.com/Intuned/cookbook/tree/main/typescript-examples/computer-use).
</Tip>

#### Key differences

**Same AI trade-offs.** Like the AI automation libraries above, computer use models control the browser with AI at runtime. This means higher cost, slower speed, less consistency, and harder debugging. See [Why deterministic code for repetitive tasks](#why-deterministic-code-for-repetitive-tasks) for details.

**Control.** Computer use models see the screen as pixels, not structured HTML. This means no direct DOM access, no network interception, and no fine-grained control over page state—only high-level actions (click, type, scroll) without the granular control that Playwright provides.

#### When computer use models make sense

Computer use models work well for one-off tasks. For repetitive automation—scrapers that run on a schedule, browser-based RPA workflows that execute thousands of times—deterministic code is more reliable and cost-effective.

AI can still help you write and maintain that code, but it doesn't need to run at execution time. That's the approach we take with [Intuned Agent](/docs/main/02-intuned-agent/overview).

## Intuned Agent

This section covers how Intuned Agent—an AI agent that builds and maintains browser automations—compares to other AI-powered approaches.

### General coding agents vs. Intuned Agent

**Examples:** Claude Code, Codex, Cursor

General-purpose coding agents can control a browser and write Playwright code, but they aren't purpose-built for browser automation. Intuned Agent is custom-built for this domain—with specialized tools for writing high-quality automation code and deep integration with the Intuned platform.

#### What Intuned Agent adds

**Purpose-built tooling.** Intuned Agent has custom tools designed specifically for browser automation codegen. For scrapers, for example, the agent handles pagination, infinite scroll, iframes, and detail page navigation natively—patterns that are specific to scraping and that general agents treat as generic coding problems.

**Deep platform integration.** The agent works inside the Intuned platform—it manages [AuthSessions](/docs/main/02-features/auth-sessions), configures [Jobs](/docs/main/02-features/jobs-batched-executions), reads Run and JobRun results, downloads and analyzes browser traces, and tests on the same infrastructure your automations use in production, including [stealth mode and captcha solving](/docs/main/02-features/stealth-mode-captcha-solving-proxies).

**Background validation at scale.** Thorough validation—pagination, edge cases, dynamic content—can take 30–60 minutes. General agents time out or lose context. Intuned Agent runs validation as a background task, for as long as needed.

**[Self-healing projects](/docs/main/02-intuned-agent/self-healing-projects).** Once deployed, Intuned Agent monitors your automations, detects issues, and applies fixes automatically—closing the loop from build to maintenance.

**[Run it in your own coding agent](/docs/main/02-intuned-agent/external-agent).** Prefer your own setup? The Intuned Agent Plugin (private preview, available on request) brings this same purpose-built tooling and platform integration into Claude Code or Codex, so you can build, edit, and debug automations locally.

### Director.ai vs. Intuned Agent

Director generates Stagehand scripts—automation code that uses AI to control the browser at runtime. Intuned Agent generates deterministic Playwright code by default.

The difference starts with what runs at execution time. Stagehand scripts inherit all the trade-offs covered in [Why deterministic code for repetitive tasks](#why-deterministic-code-for-repetitive-tasks): higher cost, slower speed, less consistency, and harder debugging. For repetitive automation, deterministic code is more reliable and cost-effective.

Beyond code output, Intuned Agent also validates automations on [production infrastructure](/docs/main/02-features/stealth-mode-captcha-solving-proxies), manages full project configuration including [AuthSessions](/docs/main/02-features/auth-sessions) and dependencies, and can [maintain automations automatically](/docs/main/02-intuned-agent/self-healing-projects) after deployment.

## Related resources

* [How the platform works](/docs/main/00-getting-started/how-intuned-works) — Understand Projects, Runs, and Jobs
* [Flexible automation](/docs/main/02-features/flexible-automation) — Deterministic, AI-driven, or hybrid approaches
* [Intuned Agent](/docs/main/02-intuned-agent/overview) — An AI agent that builds and maintains browser automations
* [Self-healing projects](/docs/main/02-intuned-agent/self-healing-projects) — Automatic issue detection and fixes
* [Stealth mode, captcha solving, and proxies](/docs/main/02-features/stealth-mode-captcha-solving-proxies) — Avoid bot detection
* [AuthSessions](/docs/main/02-features/auth-sessions) — Handle login flows and session management
* [Browser Use integration](/docs/main/04-integrations/browser-use) — Run Browser Use agents on Intuned
* [Stagehand integration](/docs/main/04-integrations/stagehand) — Use Stagehand for AI-powered automation
* [Observability, monitoring, and logs](/docs/main/02-features/observability-monitoring-logs) — Debug at the automation level
* [Cookbook](https://github.com/Intuned/cookbook) — Starter projects and examples


# Playwright for automation
Source: https://intunedhq.com/docs/main/01-learn/deep-dives/playwright



Intuned's runtime is built on top of [Playwright](https://playwright.dev/), the open-source browser automation framework. When you're writing deterministic automation code, you'll mostly be using Playwright directly.

In Intuned, you deploy code-based **Projects** containing **APIs**. Each API is a handler function that receives a browser `page` and `context`, along with any parameters you define:

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { BrowserContext, Page } from "playwright";

  interface Params {
    // Define your input parameters here
  }

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    // Your automation code here
    return {};
  }
  ```

  ```python Python theme={null}
  from playwright.async_api import Page, BrowserContext
  from typing import TypedDict, Any

  class Params(TypedDict):
      # Define your input parameters here
      pass

  async def automation(page: Page, params: dict[str, Any] | None = None, **_kwargs):
      # Your automation code here
      return {}
  ```
</CodeGroup>

**Page** is your browser tab—where you navigate, find elements, interact, and extract data. **BrowserContext** is an isolated browser session with its own cookies, storage, and cache (like an incognito window). Intuned handles browser initialization and context creation; you just use the `page` and `context` you receive.

This guide covers Playwright concepts and patterns you'll use when building automations. If you want to jump straight into code, check out the [Playwright basics project](https://github.com/intuned/cookbook/tree/main/typescript-examples/playwright-basics) in the Intuned TypeScript cookbook or the [Playwright basics project](https://github.com/intuned/cookbook/tree/main/python-examples/playwright-basics) in the Python cookbook.

## Basics

### Navigation

Navigate to a page with `goto()`:

<CodeGroup>
  ```typescript TypeScript theme={null}
  await page.goto("https://books.toscrape.com/");
  ```

  ```python Python theme={null}
  await page.goto("https://books.toscrape.com/")
  ```
</CodeGroup>

#### Wait for page load

After navigation, you may need to wait for the page to fully load. Playwright provides `waitForLoadState()`:

| State              | What it waits for                                       |
| ------------------ | ------------------------------------------------------- |
| `load`             | Full page load including images and subframes (default) |
| `domcontentloaded` | DOM is ready, but resources may still be loading        |
| `networkidle`      | No network connections for at least 500ms               |

<CodeGroup>
  ```typescript TypeScript theme={null}
  await page.goto("https://example.com");

  // Wait for full page load (default behavior)
  await page.waitForLoadState("load");

  // Wait for DOM to be ready (faster, use when you don't need images)
  await page.waitForLoadState("domcontentloaded");

  // Wait for network to be idle (use for SPAs or pages with async data)
  await page.waitForLoadState("networkidle");
  ```

  ```python Python theme={null}
  await page.goto("https://example.com")

  # Wait for full page load (default behavior)
  await page.wait_for_load_state("load")

  # Wait for DOM to be ready (faster, use when you don't need images)
  await page.wait_for_load_state("domcontentloaded")

  # Wait for network to be idle (use for SPAs or pages with async data)
  await page.wait_for_load_state("networkidle")
  ```
</CodeGroup>

#### Wait for URL

Wait for the page to navigate to a specific URL pattern:

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Wait for exact URL
  await page.waitForURL("https://example.com/dashboard");

  // Wait for URL pattern (glob)
  await page.waitForURL("**/dashboard/**");

  // Wait for URL matching regex
  await page.waitForURL(/\/order\/\d+/);
  ```

  ```python Python theme={null}
  # Wait for exact URL
  await page.wait_for_url("https://example.com/dashboard")

  # Wait for URL pattern (glob)
  await page.wait_for_url("**/dashboard/**")

  # Wait for URL matching regex
  import re
  await page.wait_for_url(re.compile(r"/order/\d+"))
  ```
</CodeGroup>

#### Create new pages

Intuned initializes one page by default—the `page` you receive in your handler. You can create additional pages to run operations in parallel. Both pages run in the same browser on the same machine:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const newPage = await context.newPage();
  await newPage.goto("https://example.com");
  const pageTitle = await newPage.title();
  ```

  ```python Python theme={null}
  new_page = await page.context.new_page()
  await new_page.goto("https://example.com")
  page_title = await new_page.title()
  ```
</CodeGroup>

#### Handle new tabs and popups

When a click opens a new tab (like `target="_blank"` links), capture it with `waitForEvent`:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const [newPage] = await Promise.all([
    context.waitForEvent("page"),
    page.locator(".product_pod h3 a").first().click(),
  ]);

  await newPage.waitForLoadState("domcontentloaded");
  const bookTitle = await newPage.locator(".product_main h1").innerText();
  ```

  ```python Python theme={null}
  async with page.context.expect_page() as new_page_info:
      await page.locator(".product_pod h3 a").first.click()

  new_page = await new_page_info.value
  await new_page.wait_for_load_state("domcontentloaded")
  book_title = await new_page.locator(".product_main h1").inner_text()
  ```
</CodeGroup>

For popups, use the `popup` event:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const [popup] = await Promise.all([
    page.waitForEvent("popup"),
    page.locator("#open-popup-button").click(),
  ]);

  await popup.waitForLoadState("domcontentloaded");
  ```

  ```python Python theme={null}
  async with page.expect_popup() as popup_info:
      await page.locator("#open-popup-button").click()

  popup = await popup_info.value
  await popup.wait_for_load_state("domcontentloaded")
  ```
</CodeGroup>

### Finding elements

Locators are the primary way to find elements in Playwright. They're lazy—not evaluated until you interact with them:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const button = page.locator("button#submit"); // Just a locator, no query yet
  await button.click(); // Now it's evaluated
  ```

  ```python Python theme={null}
  button = page.locator("button#submit")  # Just a locator, no query yet
  await button.click()  # Now it's evaluated
  ```
</CodeGroup>

#### CSS vs XPath selectors

| Feature     | CSS selector                     | XPath selector          |
| ----------- | -------------------------------- | ----------------------- |
| Syntax      | `div.class #id`                  | `//div[@class='class']` |
| Best for    | Simple queries, styling patterns | Complex DOM traversals  |
| Performance | Slightly faster                  | Slightly slower         |

<CodeGroup>
  ```typescript TypeScript theme={null}
  const cssLocator = page.locator("div.container > button");
  const xpathLocator = page.locator("//div[@class='container']/button");
  ```

  ```python Python theme={null}
  css_locator = page.locator("div.container > button")
  xpath_locator = page.locator("//div[@class='container']/button")
  ```
</CodeGroup>

#### CSS selector extensions

When using `page.locator()` with CSS selectors, Playwright adds special pseudo-classes that aren't available in standard CSS:

| Extension           | Description                      | Example                    |
| ------------------- | -------------------------------- | -------------------------- |
| `:has-text("text")` | Contains text anywhere inside    | `div:has-text("Welcome")`  |
| `:text("text")`     | Smallest element containing text | `span:text("Price")`       |
| `:text-is("text")`  | Exact text match                 | `button:text-is("Submit")` |
| `:visible`          | Only visible elements            | `button:visible`           |
| `:has(selector)`    | Contains matching child          | `div:has(> img)`           |

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Find div containing "Welcome" text
  const welcomeDiv = page.locator('div:has-text("Welcome")');

  // Find visible buttons only
  const visibleButtons = page.locator("button:visible");

  // Find cards that contain images
  const imageCards = page.locator("div.card:has(img)");
  ```

  ```python Python theme={null}
  # Find div containing "Welcome" text
  welcome_div = page.locator('div:has-text("Welcome")')

  # Find visible buttons only
  visible_buttons = page.locator("button:visible")

  # Find cards that contain images
  image_cards = page.locator("div.card:has(img)")
  ```
</CodeGroup>

For more details, see the [Playwright locators documentation](https://playwright.dev/docs/other-locators).

#### Semantic locators

Instead of CSS selectors, Playwright provides methods that find elements by their semantic meaning—role, label, placeholder, or test ID. These are more resilient to markup changes and make your code easier to read:

**`getByRole()`** — Find by ARIA role and accessible name:

<CodeGroup>
  ```typescript TypeScript theme={null}
  await page.getByRole("button", { name: "Submit" }).click();
  await page.getByRole("link", { name: "Learn more" }).click();
  await page.getByRole("checkbox", { name: "Accept terms" }).check();
  ```

  ```python Python theme={null}
  await page.get_by_role("button", name="Submit").click()
  await page.get_by_role("link", name="Learn more").click()
  await page.get_by_role("checkbox", name="Accept terms").check()
  ```
</CodeGroup>

**`getByText()`** — Find by visible text:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const link = page.getByText("Learn more", { exact: true });
  ```

  ```python Python theme={null}
  link = page.get_by_text("Learn more", exact=True)
  ```
</CodeGroup>

**`getByLabel()`** — Find form fields by their label:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const emailInput = page.getByLabel("Email");
  await emailInput.fill("example@example.com");
  ```

  ```python Python theme={null}
  email_input = page.get_by_label("Email")
  await email_input.fill("example@example.com")
  ```
</CodeGroup>

**`getByPlaceholder()`** — Find inputs by placeholder text:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const searchInput = page.getByPlaceholder("Search");
  await searchInput.fill("Playwright");
  ```

  ```python Python theme={null}
  search_input = page.get_by_placeholder("Search")
  await search_input.fill("Playwright")
  ```
</CodeGroup>

**`getByAltText()`** — Find images by alt text:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const logo = page.getByAltText("Company Logo");
  ```

  ```python Python theme={null}
  logo = page.get_by_alt_text("Company Logo")
  ```
</CodeGroup>

**`getByTitle()`** — Find by title attribute:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const helpIcon = page.getByTitle("Help");
  ```

  ```python Python theme={null}
  help_icon = page.get_by_title("Help")
  ```
</CodeGroup>

**`getByTestId()`** — Find by `data-testid` attribute:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const submitBtn = page.getByTestId("submit-button");
  ```

  ```python Python theme={null}
  submit_btn = page.get_by_test_id("submit-button")
  ```
</CodeGroup>

#### Chaining and filtering

When a locator matches multiple elements, narrow it down:

| Method                | What it does                                                      |
| --------------------- | ----------------------------------------------------------------- |
| `.first()` / `.first` | First matching element (method in TypeScript, property in Python) |
| `.last()` / `.last`   | Last matching element (method in TypeScript, property in Python)  |
| `.nth(index)`         | Element at specific position (0-indexed)                          |
| `.filter()`           | Add conditions to narrow results                                  |

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Get the first button
  const firstButton = page.getByRole("button").first();

  // Get the 3rd list item (index 2)
  const thirdItem = page.getByRole("listitem").nth(2);

  // Filter buttons by text
  const saveButton = page.getByRole("button").filter({ hasText: "Save" });

  // Filter by containing a specific child element
  const cardWithImage = page.locator(".card").filter({ has: page.locator("img") });

  // Exclude elements
  const nonPromoCards = page.locator(".card").filter({ hasNot: page.locator(".promo-badge") });
  ```

  ```python Python theme={null}
  # Get the first button
  first_button = page.get_by_role("button").first

  # Get the 3rd list item (index 2)
  third_item = page.get_by_role("listitem").nth(2)

  # Filter buttons by text
  save_button = page.get_by_role("button").filter(has_text="Save")

  # Filter by containing a specific child element
  card_with_image = page.locator(".card").filter(has=page.locator("img"))

  # Exclude elements
  non_promo_cards = page.locator(".card").filter(has_not=page.locator(".promo-badge"))
  ```
</CodeGroup>

#### Combining locators

Combine locators with `and()` and `or()`:

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Element must match both conditions
  const enabledSubmit = page.getByRole("button", { name: "Submit" }).and(page.locator(":not([disabled])"));

  // Element can match either condition
  const actionButton = page.getByRole("button", { name: "Save" }).or(page.getByRole("button", { name: "Update" }));
  ```

  ```python Python theme={null}
  # Element must match both conditions
  enabled_submit = page.get_by_role("button", name="Submit").and_(page.locator(":not([disabled])"))

  # Element can match either condition
  action_button = page.get_by_role("button", name="Save").or_(page.get_by_role("button", name="Update"))
  ```
</CodeGroup>

#### Checking element state

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Check visibility
  const isVisible = await page.getByRole("button", { name: "Login" }).isVisible();
  const isHidden = await page.getByText("Loading...").isHidden();

  // Check other states
  const isEnabled = await page.locator("#submit").isEnabled();
  const isChecked = await page.locator("#agree").isChecked();

  // Count matching elements
  const itemCount = await page.getByRole("listitem").count();
  ```

  ```python Python theme={null}
  # Check visibility
  is_visible = await page.get_by_role("button", name="Login").is_visible()
  is_hidden = await page.get_by_text("Loading...").is_hidden()

  # Check other states
  is_enabled = await page.locator("#submit").is_enabled()
  is_checked = await page.locator("#agree").is_checked()

  # Count matching elements
  item_count = await page.get_by_role("listitem").count()
  ```
</CodeGroup>

<Warning>
  **Strict mode**: Playwright requires locators to match exactly one element. If your locator matches zero or multiple elements, you'll get a strict mode violation error. Use `.first()`, `.nth()`, or `.filter()` to be specific.
</Warning>

### Auto-waiting and timeouts

Playwright automatically waits for elements to be ready before performing actions. This auto-waiting happens within a configurable timeout (30 seconds by default). Understanding this helps you write reliable automations without unnecessary delays.

#### What Playwright waits for

When you call an action like `click()` or `fill()`, Playwright automatically waits until the element is:

| Check               | Description                                                    |
| ------------------- | -------------------------------------------------------------- |
| **Attached**        | Element exists in the DOM                                      |
| **Visible**         | Element has non-empty bounding box and no `visibility: hidden` |
| **Stable**          | Element isn't animating (same position for 2 animation frames) |
| **Enabled**         | Element isn't disabled                                         |
| **Receives events** | Element isn't obscured by other elements                       |

This means you rarely need explicit waits before actions:

<CodeGroup>
  ```typescript TypeScript theme={null}
  // No need to wait—Playwright handles it
  await page.getByRole("button", { name: "Submit" }).click();
  ```

  ```python Python theme={null}
  # No need to wait—Playwright handles it
  await page.get_by_role("button", name="Submit").click()
  ```
</CodeGroup>

#### When you need explicit waits

Use explicit waits when:

* Waiting for elements to appear or disappear (loading spinners)
* Waiting for navigation to complete
* Waiting for network requests to finish

**Wait for elements:**

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Wait until element is visible
  await page.waitForSelector(".product_pod h3 a", { state: "visible", timeout: 5000 });

  // Wait until element is hidden
  await page.waitForSelector(".loading-spinner", { state: "hidden" });

  // Wait until element is removed from DOM
  await page.waitForSelector(".modal", { state: "detached" });
  ```

  ```python Python theme={null}
  # Wait until element is visible
  await page.wait_for_selector(".product_pod h3 a", state="visible", timeout=5000)

  # Wait until element is hidden
  await page.wait_for_selector(".loading-spinner", state="hidden")

  # Wait until element is removed from DOM
  await page.wait_for_selector(".modal", state="detached")
  ```
</CodeGroup>

**Wait for network requests:**

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Wait for a specific API response
  const response = await page.waitForResponse("**/api/products");
  const data = await response.json();

  // Wait for response matching a condition
  const response = await page.waitForResponse(
    (resp) => resp.url().includes("/api/") && resp.status() === 200
  );

  // Click and wait for API response
  const [response] = await Promise.all([
    page.waitForResponse("**/api/search"),
    page.locator("#search-button").click(),
  ]);
  ```

  ```python Python theme={null}
  # Wait for a specific API response
  response = await page.wait_for_response("**/api/products")
  data = await response.json()

  # Wait for response matching a condition
  response = await page.wait_for_response(
      lambda resp: "/api/" in resp.url and resp.status == 200
  )

  # Click and wait for API response
  async with page.expect_response("**/api/search") as response_info:
      await page.locator("#search-button").click()
  response = await response_info.value
  ```
</CodeGroup>

<Note>
  Avoid `waitForTimeout()` (fixed delays). Waiting for specific elements or network states is more reliable and faster.
</Note>

For more reliable waiting, the Intuned SDK offers [`waitForDomSettled`](/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/waitForDomSettled) and [`withNetworkSettledWait`](/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/withNetworkSettledWait) helpers.

#### Configuring timeouts

The timeout controls how long Playwright waits during auto-waiting before throwing an error. The default is 30 seconds.

**Default timeout** — Set for all actions on a page:

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Set default timeout to 10 seconds for all actions
  page.setDefaultTimeout(10000);

  // Set default timeout for navigation only
  page.setDefaultNavigationTimeout(30000);
  ```

  ```python Python theme={null}
  # Set default timeout to 10 seconds for all actions
  page.set_default_timeout(10000)

  # Set default timeout for navigation only
  page.set_default_navigation_timeout(30000)
  ```
</CodeGroup>

**Per-action timeout** — Override for specific actions:

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Wait up to 60 seconds for this specific action
  await page.locator("#slow-element").click({ timeout: 60000 });

  // Wait up to 5 seconds for element to appear
  await page.waitForSelector(".loaded", { timeout: 5000 });
  ```

  ```python Python theme={null}
  # Wait up to 60 seconds for this specific action
  await page.locator("#slow-element").click(timeout=60000)

  # Wait up to 5 seconds for element to appear
  await page.wait_for_selector(".loaded", timeout=5000)
  ```
</CodeGroup>

## Extracting data

### Text content

Extract the visible text from elements:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const title = await page.locator(".product_pod h3 a").first().textContent();
  ```

  ```python Python theme={null}
  title = await page.locator(".product_pod h3 a").first.text_content()
  ```
</CodeGroup>

| Method          | What it returns                                          |
| --------------- | -------------------------------------------------------- |
| `textContent()` | All text including hidden elements, preserves whitespace |
| `innerText()`   | Visible text only, normalized whitespace                 |
| `innerHTML()`   | Raw HTML content                                         |

### Attributes

Extract values from HTML attributes like `href`, `src`, `data-*`:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const href = await page.locator(".product_pod h3 a").first().getAttribute("href");
  const imageSrc = await page.locator("img.product-image").getAttribute("src");
  const productId = await page.locator(".product").getAttribute("data-product-id");
  ```

  ```python Python theme={null}
  href = await page.locator(".product_pod h3 a").first.get_attribute("href")
  image_src = await page.locator("img.product-image").get_attribute("src")
  product_id = await page.locator(".product").get_attribute("data-product-id")
  ```
</CodeGroup>

### Bulk extraction

Use `all()` to get all matching elements as an array, then extract data from each:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const items = await page.locator(".product_pod").all();
  for (const item of items) {
    const title = await item.locator("h3 a").textContent();
    const price = await item.locator(".price_color").textContent();
    console.log({ title, price });
  }
  ```

  ```python Python theme={null}
  items = await page.locator(".product_pod").all()
  for item in items:
      title = await item.locator("h3 a").text_content()
      price = await item.locator(".price_color").text_content()
      print({"title": title, "price": price})
  ```
</CodeGroup>

<Note>
  `all()` returns an empty array if no elements match. If you need to wait for elements first, use `waitForSelector()` before calling `all()`.
</Note>

For simple cases where you only need text from a single selector, `allTextContents()` and `allInnerTexts()` are convenience methods:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const allTitles = await page.locator(".product_pod h3 a").allTextContents();
  const allVisibleTexts = await page.locator(".product_pod h3 a").allInnerTexts();
  ```

  ```python Python theme={null}
  all_titles = await page.locator(".product_pod h3 a").all_text_contents()
  all_visible_texts = await page.locator(".product_pod h3 a").all_inner_texts()
  ```
</CodeGroup>

### Lists and iteration

Loop through multiple elements using `count()` and `nth()`:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const items = page.locator(".product_pod h3 a");
  const count = await items.count();

  const titles: string[] = [];
  for (let i = 0; i < count; i++) {
    const title = await items.nth(i).innerText();
    titles.push(title);
  }
  ```

  ```python Python theme={null}
  items = page.locator(".product_pod h3 a")
  count = await items.count()

  titles = []
  for i in range(count):
      title = await items.nth(i).inner_text()
      titles.append(title)
  ```
</CodeGroup>

### Input values

In rare cases, you may need to read the current value from form inputs—for example, when scraping pre-filled forms or verifying form state:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const searchValue = await page.locator("#search-input").inputValue();
  ```

  ```python Python theme={null}
  search_value = await page.locator("#search-input").input_value()
  ```
</CodeGroup>

### Related Intuned SDK helpers

The Intuned SDK provides helper functions that simplify common data extraction patterns. These handle edge cases and reduce boilerplate:

| Helper                                                                                                 | What it does                                     |
| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------ |
| [`scrollToLoadContent`](/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/scrollToLoadContent) | Scrolls to load infinite scroll content          |
| [`clickUntilExhausted`](/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/clickUntilExhausted) | Clicks "Load more" buttons until no more content |
| [`extractMarkdown`](/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/extractMarkdown)         | Extracts page content as clean markdown          |

See the [Pagination recipe](/docs/main/01-learn/recipes/pagination) for multi-page scraping patterns.

## Performing actions

### Clicking

<CodeGroup>
  ```typescript TypeScript theme={null}
  await page.locator('a:has-text("Travel")').click();

  // Wait for navigation after click
  await page.waitForLoadState("networkidle");
  ```

  ```python Python theme={null}
  await page.locator('a:has-text("Travel")').click()

  # Wait for navigation after click
  await page.wait_for_load_state("networkidle")
  ```
</CodeGroup>

#### Double-click

<CodeGroup>
  ```typescript TypeScript theme={null}
  await page.locator(".editable-cell").dblclick();
  ```

  ```python Python theme={null}
  await page.locator(".editable-cell").dblclick()
  ```
</CodeGroup>

#### Hover

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Hover to reveal dropdown menu
  await page.locator(".nav-menu").hover();
  await page.locator(".dropdown-item").click();
  ```

  ```python Python theme={null}
  # Hover to reveal dropdown menu
  await page.locator(".nav-menu").hover()
  await page.locator(".dropdown-item").click()
  ```
</CodeGroup>

#### Force click

Use `force: true` when you know the element is there but Playwright's actionability checks fail (e.g., element is covered by an overlay you want to ignore):

<CodeGroup>
  ```typescript TypeScript theme={null}
  await page.locator("#hidden-button").click({ force: true });
  ```

  ```python Python theme={null}
  await page.locator("#hidden-button").click(force=True)
  ```
</CodeGroup>

<Warning>
  Use `force` sparingly. If you're using it frequently, there may be a better way to handle the interaction.
</Warning>

### Text input

<CodeGroup>
  ```typescript TypeScript theme={null}
  await page.locator("input[name='search']").fill("test text");

  // Clear field before filling
  await page.locator("input[name='search']").clear();
  await page.locator("input[name='search']").fill("new text");
  ```

  ```python Python theme={null}
  await page.locator("input[name='search']").fill("test text")

  # Clear field before filling
  await page.locator("input[name='search']").clear()
  await page.locator("input[name='search']").fill("new text")
  ```
</CodeGroup>

#### Type character by character

Use `pressSequentially()` when a page has special keyboard handling that doesn't work with `fill()`:

<CodeGroup>
  ```typescript TypeScript theme={null}
  await page.locator("#autocomplete-input").pressSequentially("new york", { delay: 100 });
  ```

  ```python Python theme={null}
  await page.locator("#autocomplete-input").press_sequentially("new york", delay=100)
  ```
</CodeGroup>

### Keyboard actions

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Press a single key
  await page.keyboard.press("Enter");
  await page.keyboard.press("Escape");

  // Key combinations
  await page.keyboard.press("Control+a");  // Select all
  await page.keyboard.press("Control+c");  // Copy
  await page.keyboard.press("Control+v");  // Paste

  // Press key on a specific element
  await page.locator("#search-input").press("Enter");

  // Type text with full key events
  await page.keyboard.type("Hello World");
  ```

  ```python Python theme={null}
  # Press a single key
  await page.keyboard.press("Enter")
  await page.keyboard.press("Escape")

  # Key combinations
  await page.keyboard.press("Control+a")  # Select all
  await page.keyboard.press("Control+c")  # Copy
  await page.keyboard.press("Control+v")  # Paste

  # Press key on a specific element
  await page.locator("#search-input").press("Enter")

  # Type text with full key events
  await page.keyboard.type("Hello World")
  ```
</CodeGroup>

### Forms

#### Dropdowns

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Select by value
  await page.locator("select#sort-by").selectOption("price-desc");

  // Select by label text
  await page.locator("#dropdown").selectOption({ label: "Option 2" });

  // Select by index
  await page.locator("#dropdown").selectOption({ index: 2 });

  // Select multiple options
  await page.locator("#multi-select").selectOption(["option1", "option2"]);
  ```

  ```python Python theme={null}
  # Select by value
  await page.locator("select#sort-by").select_option("price-desc")

  # Select by label text
  await page.locator("#dropdown").select_option(label="Option 2")

  # Select by index
  await page.locator("#dropdown").select_option(index=2)

  # Select multiple options
  await page.locator("#multi-select").select_option(["option1", "option2"])
  ```
</CodeGroup>

#### Checkboxes and radio buttons

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Check a checkbox or radio button
  await page.locator("input[name='rating'][value='4']").check();

  // Uncheck a checkbox
  await page.locator("#newsletter").uncheck();

  // Set checkbox to specific state
  await page.locator("#terms").setChecked(true);
  ```

  ```python Python theme={null}
  # Check a checkbox or radio button
  await page.locator("input[name='rating'][value='4']").check()

  # Uncheck a checkbox
  await page.locator("#newsletter").uncheck()

  # Set checkbox to specific state
  await page.locator("#terms").set_checked(True)
  ```
</CodeGroup>

### Handling dialogs

JavaScript dialogs (`alert`, `confirm`, `prompt`) block the page until handled. Set up a listener *before* the action that triggers the dialog:

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Handle an alert
  page.on("dialog", async (dialog) => {
    console.log(dialog.message());
    await dialog.accept();
  });
  await page.locator("#show-alert").click();

  // Handle a confirm dialog
  page.on("dialog", async (dialog) => {
    if (dialog.type() === "confirm") {
      await dialog.accept(); // Click OK
      // or: await dialog.dismiss(); // Click Cancel
    }
  });

  // Handle a prompt dialog with input
  page.on("dialog", async (dialog) => {
    if (dialog.type() === "prompt") {
      await dialog.accept("My answer"); // Enter text and click OK
    }
  });
  ```

  ```python Python theme={null}
  # Handle an alert
  async def handle_dialog(dialog):
      print(dialog.message)
      await dialog.accept()

  page.on("dialog", handle_dialog)
  await page.locator("#show-alert").click()

  # Handle a confirm dialog
  async def handle_confirm(dialog):
      if dialog.type == "confirm":
          await dialog.accept()  # Click OK
          # or: await dialog.dismiss()  # Click Cancel

  page.on("dialog", handle_confirm)

  # Handle a prompt dialog with input
  async def handle_prompt(dialog):
      if dialog.type == "prompt":
          await dialog.accept("My answer")  # Enter text and click OK

  page.on("dialog", handle_prompt)
  ```
</CodeGroup>

For one-time dialog handling:

<CodeGroup>
  ```typescript TypeScript theme={null}
  page.once("dialog", (dialog) => dialog.accept());
  await page.locator("#delete-button").click();
  ```

  ```python Python theme={null}
  page.once("dialog", lambda dialog: dialog.accept())
  await page.locator("#delete-button").click()
  ```
</CodeGroup>

<Warning>
  If you don't handle a dialog, it will auto-dismiss, but the page may hang waiting for it. Always set up dialog handlers before triggering actions that show dialogs.
</Warning>

### File uploads

<CodeGroup>
  ```typescript TypeScript theme={null}
  await page.setInputFiles("#file-input", "path/to/file.pdf");

  // Multiple files
  await page.setInputFiles("#file-input", ["file1.pdf", "file2.pdf"]);

  // Clear file selection
  await page.setInputFiles("#file-input", []);
  ```

  ```python Python theme={null}
  await page.set_input_files("#file-input", "path/to/file.pdf")

  # Multiple files
  await page.set_input_files("#file-input", ["file1.pdf", "file2.pdf"])

  # Clear file selection
  await page.set_input_files("#file-input", [])
  ```
</CodeGroup>

### File downloads

Listen for the `download` event before triggering the download:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const [download] = await Promise.all([
    page.waitForEvent("download"),
    page.locator("#download-button").click(),
  ]);

  const filePath = await download.path();
  const fileName = download.suggestedFilename();
  ```

  ```python Python theme={null}
  async with page.expect_download() as download_info:
      await page.locator("#download-button").click()

  download = await download_info.value
  file_path = await download.path()
  file_name = download.suggested_filename
  ```
</CodeGroup>

The Intuned SDK provides helpers that simplify file handling and cloud storage:

| Helper                                                                                       | What it does                                   |
| -------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| [`downloadFile`](/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/downloadFile)     | Downloads a file from a URL                    |
| [`uploadFileToS3`](/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/uploadFileToS3) | Uploads a file to your S3 bucket               |
| [`saveFileToS3`](/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/saveFileToS3)     | Downloads and uploads a file to S3 in one step |

See the [download file recipe](/docs/main/01-learn/recipes/download-file) and [upload to S3 recipe](/docs/main/01-learn/recipes/upload-files).

### Complete form filling example

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    await page.goto("https://demoqa.com/automation-practice-form");

    // Text inputs
    await page.getByPlaceholder("First Name").fill("John");
    await page.getByPlaceholder("Last Name").fill("Doe");
    await page.getByPlaceholder("name@example.com").fill("john.doe@test.com");
    await page.getByPlaceholder("Mobile Number").fill("0791234567");

    // Radio button
    await page.getByText("Male", { exact: true }).click();

    // Date picker
    await page.locator("#dateOfBirthInput").click();
    await page.getByRole("option", { name: "15" }).click();

    // Autocomplete field
    await page.locator("#subjectsInput").fill("Maths");
    await page.keyboard.press("Enter");

    // Checkbox
    await page.getByText("Sports").click();

    // File upload
    await page.setInputFiles("#uploadPicture", "test-files/sample.png");

    // Textarea
    await page.locator("#currentAddress").fill("Test Street, Automation City");

    // Submit
    await page.locator("#submit").click();

    return { submitted: true };
  }
  ```

  ```python Python theme={null}
  from playwright.async_api import Page, BrowserContext
  from typing import Any

  async def automation(page: Page, params: dict[str, Any] | None = None, **_kwargs):
      await page.goto("https://demoqa.com/automation-practice-form")

      # Text inputs
      await page.get_by_placeholder("First Name").fill("John")
      await page.get_by_placeholder("Last Name").fill("Doe")
      await page.get_by_placeholder("name@example.com").fill("john.doe@test.com")
      await page.get_by_placeholder("Mobile Number").fill("0791234567")

      # Radio button
      await page.get_by_text("Male", exact=True).click()

      # Date picker
      await page.locator("#dateOfBirthInput").click()
      await page.get_by_role("option", name="15").click()

      # Autocomplete field
      await page.locator("#subjectsInput").fill("Maths")
      await page.keyboard.press("Enter")

      # Checkbox
      await page.get_by_text("Sports").click()

      # File upload
      await page.set_input_files("#uploadPicture", "test-files/sample.png")

      # Textarea
      await page.locator("#currentAddress").fill("Test Street, Automation City")

      # Submit
      await page.locator("#submit").click()

      return {"submitted": True}
  ```
</CodeGroup>

## Advanced topics

### Working with frames

Frames (iframes) embed a separate webpage inside another. You can't interact with iframe content directly from the main page—you need to enter the frame context first.

```html theme={null}
<iframe src="https://payments.example.com"></iframe>
```

Use `frameLocator()` to interact with iframe content:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const loginFrame = page.frameLocator("iframe#login-iframe");

  await loginFrame.locator("input[name='username']").fill("test_user");
  await loginFrame.locator("input[name='password']").fill("secret");
  await loginFrame.locator("button[type='submit']").click();
  ```

  ```python Python theme={null}
  login_frame = page.frame_locator("iframe#login-iframe")

  await login_frame.locator("input[name='username']").fill("test_user")
  await login_frame.locator("input[name='password']").fill("secret")
  await login_frame.locator("button[type='submit']").click()
  ```
</CodeGroup>

For lower-level control, use `contentFrame()`:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const iframeHandle = await page.waitForSelector("iframe#login-frame");
  const frame = await iframeHandle.contentFrame();

  if (!frame) {
    throw new Error("Frame content not available");
  }

  await frame.fill("input[name='username']", "test_user");
  ```

  ```python Python theme={null}
  iframe = await page.wait_for_selector("iframe#login-frame")
  frame = await iframe.content_frame()

  if not frame:
      raise Exception("Frame content not available")

  await frame.fill("input[name='username']", "test_user")
  ```
</CodeGroup>

<Note>
  **Shadow DOM**: Playwright automatically pierces open shadow DOM—no special handling needed. Your regular locators will find elements inside shadow roots.
</Note>

### Screenshots

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Screenshot of visible viewport
  await page.screenshot({ path: "screenshot.png" });

  // Full page screenshot (entire scrollable area)
  await page.screenshot({ path: "fullpage.png", fullPage: true });

  // Element screenshot
  await page.locator(".product-card").first().screenshot({ path: "product.png" });

  // Mask sensitive elements
  await page.screenshot({
    path: "screenshot.png",
    mask: [
      page.locator(".credit-card-number"),
      page.locator(".ssn-field"),
    ],
  });
  ```

  ```python Python theme={null}
  # Screenshot of visible viewport
  await page.screenshot(path="screenshot.png")

  # Full page screenshot (entire scrollable area)
  await page.screenshot(path="fullpage.png", full_page=True)

  # Element screenshot
  await page.locator(".product-card").first.screenshot(path="product.png")

  # Mask sensitive elements
  await page.screenshot(
      path="screenshot.png",
      mask=[
          page.locator(".credit-card-number"),
          page.locator(".ssn-field"),
      ],
  )
  ```
</CodeGroup>

### Network interception

Intercept and modify network requests using `route()`.

#### Block resources

Speed up automations by blocking unnecessary resources:

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Block images
  await page.route("**/*.{png,jpg,jpeg,gif,webp,svg}", (route) => route.abort());

  // Block by resource type
  await page.route("**/*", (route) => {
    const resourceType = route.request().resourceType();
    if (["image", "font", "stylesheet"].includes(resourceType)) {
      route.abort();
    } else {
      route.continue();
    }
  });

  // Block specific domains (analytics, ads)
  await page.route("**/*google-analytics*/**", (route) => route.abort());
  await page.route("**/*doubleclick*/**", (route) => route.abort());
  ```

  ```python Python theme={null}
  # Block images
  await page.route("**/*.{png,jpg,jpeg,gif,webp,svg}", lambda route: route.abort())

  # Block by resource type
  async def block_resources(route):
      if route.request.resource_type in ["image", "font", "stylesheet"]:
          await route.abort()
      else:
          await route.continue_()

  await page.route("**/*", block_resources)

  # Block specific domains (analytics, ads)
  await page.route("**/*google-analytics*/**", lambda route: route.abort())
  await page.route("**/*doubleclick*/**", lambda route: route.abort())
  ```
</CodeGroup>

#### Modify requests

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Add custom headers
  await page.route("**/api/**", (route) => {
    route.continue({
      headers: {
        ...route.request().headers(),
        "X-Custom-Header": "value",
      },
    });
  });
  ```

  ```python Python theme={null}
  # Add custom headers
  async def add_headers(route):
      headers = route.request.headers
      headers["X-Custom-Header"] = "value"
      await route.continue_(headers=headers)

  await page.route("**/api/**", add_headers)
  ```
</CodeGroup>

For more interception patterns, see the [network interception recipe](/docs/main/01-learn/recipes/network-interception).

### Executing JavaScript

Use `page.evaluate()` when you need to access or manipulate the DOM directly, execute custom JavaScript, or access page-level variables.

#### Scrolling

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Scroll to bottom of page
  await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));

  // Scroll by a specific amount
  await page.evaluate(() => window.scrollBy(0, 500));

  // Scroll element into view
  await page.locator("#target-element").scrollIntoViewIfNeeded();
  ```

  ```python Python theme={null}
  # Scroll to bottom of page
  await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")

  # Scroll by a specific amount
  await page.evaluate("window.scrollBy(0, 500)")

  # Scroll element into view
  await page.locator("#target-element").scroll_into_view_if_needed()
  ```
</CodeGroup>

#### DOM manipulation

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Hide all links
  await page.evaluate(() => {
    document.querySelectorAll("a").forEach(el => {
      (el as HTMLElement).style.display = "none";
    });
  });

  // Get computed styles
  const color = await page.evaluate(() => {
    const el = document.querySelector(".header");
    return window.getComputedStyle(el!).color;
  });
  ```

  ```python Python theme={null}
  # Hide all links
  await page.evaluate("""
      document.querySelectorAll('a').forEach(el => {
          el.style.display = 'none';
      })
  """)

  # Get computed styles
  color = await page.evaluate("""
      const el = document.querySelector('.header');
      window.getComputedStyle(el).color;
  """)
  ```
</CodeGroup>

#### Pass arguments to evaluate

<CodeGroup>
  ```typescript TypeScript theme={null}
  const selector = ".product";
  const result = await page.evaluate((sel) => {
    return document.querySelectorAll(sel).length;
  }, selector);
  ```

  ```python Python theme={null}
  selector = ".product"
  result = await page.evaluate(
      "(sel) => document.querySelectorAll(sel).length",
      selector
  )
  ```
</CodeGroup>

### Making API requests

Playwright provides a built-in API client through `page.request` that shares the browser's cookies and session:

| Benefit        | Description                                |
| -------------- | ------------------------------------------ |
| Shares cookies | Automatically includes session cookies     |
| Same auth      | Uses the browser's authenticated session   |
| Traceable      | Appears in Playwright traces for debugging |

<CodeGroup>
  ```typescript TypeScript theme={null}
  // GET request
  const response = await page.request.get("https://api.example.com/data", {
    headers: { Accept: "application/json" },
  });

  if (!response.ok()) {
    throw new Error(`Request failed: ${response.status()}`);
  }

  const data = await response.json();

  // POST request
  const postResponse = await page.request.post("https://api.example.com/data", {
    headers: { "Content-Type": "application/json" },
    data: { name: "Test", status: "active" },
  });
  ```

  ```python Python theme={null}
  # GET request
  response = await page.request.get(
      "https://api.example.com/data",
      headers={"Accept": "application/json"},
  )

  if not response.ok:
      raise Exception(f"Request failed: {response.status}")

  data = await response.json()

  # POST request
  post_response = await page.request.post(
      "https://api.example.com/data",
      headers={"Content-Type": "application/json"},
      data={"name": "Test", "status": "active"},
  )
  ```
</CodeGroup>

### Cookies and storage

In most cases, you won't need to manage cookies directly—especially if you use [AuthSessions](/docs/main/02-features/auth-sessions), which handle authentication and session state automatically. The methods below are included for reference when you need low-level control.

#### Read cookies

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Get all cookies
  const cookies = await context.cookies();

  // Get cookies for specific URLs
  const siteCookies = await context.cookies(["https://example.com"]);

  // Find a specific cookie
  const sessionCookie = cookies.find((c) => c.name === "session_id");
  ```

  ```python Python theme={null}
  # Get all cookies
  cookies = await context.cookies()

  # Get cookies for specific URLs
  site_cookies = await context.cookies(["https://example.com"])

  # Find a specific cookie
  session_cookie = next((c for c in cookies if c["name"] == "session_id"), None)
  ```
</CodeGroup>

#### Set cookies

<CodeGroup>
  ```typescript TypeScript theme={null}
  await context.addCookies([
    {
      name: "session_id",
      value: "abc123",
      domain: ".example.com",
      path: "/",
    },
    {
      name: "preferences",
      value: "dark_mode",
      domain: ".example.com",
      path: "/",
      expires: Math.floor(Date.now() / 1000) + 86400, // 1 day from now
    },
  ]);
  ```

  ```python Python theme={null}
  await context.add_cookies([
      {
          "name": "session_id",
          "value": "abc123",
          "domain": ".example.com",
          "path": "/",
      },
      {
          "name": "preferences",
          "value": "dark_mode",
          "domain": ".example.com",
          "path": "/",
          "expires": int(time.time()) + 86400,  # 1 day from now
      },
  ])
  ```
</CodeGroup>

#### Clear cookies

<CodeGroup>
  ```typescript TypeScript theme={null}
  await context.clearCookies();
  ```

  ```python Python theme={null}
  await context.clear_cookies()
  ```
</CodeGroup>

#### Access localStorage and sessionStorage

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Get localStorage value
  const token = await page.evaluate(() => localStorage.getItem("authToken"));

  // Set localStorage value
  await page.evaluate(() => localStorage.setItem("theme", "dark"));

  // Get all localStorage
  const allStorage = await page.evaluate(() => JSON.stringify(localStorage));

  // Clear localStorage
  await page.evaluate(() => localStorage.clear());

  // Same methods work for sessionStorage
  const sessionData = await page.evaluate(() => sessionStorage.getItem("tempData"));
  ```

  ```python Python theme={null}
  # Get localStorage value
  token = await page.evaluate("localStorage.getItem('authToken')")

  # Set localStorage value
  await page.evaluate("localStorage.setItem('theme', 'dark')")

  # Get all localStorage
  all_storage = await page.evaluate("JSON.stringify(localStorage)")

  # Clear localStorage
  await page.evaluate("localStorage.clear()")

  # Same methods work for sessionStorage
  session_data = await page.evaluate("sessionStorage.getItem('tempData')")
  ```
</CodeGroup>

### Drag and drop

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Drag element to target
  await page.locator("#draggable").dragTo(page.locator("#drop-zone"));

  // With precise positioning
  await page.locator("#draggable").dragTo(page.locator("#drop-zone"), {
    sourcePosition: { x: 10, y: 10 },
    targetPosition: { x: 50, y: 50 },
  });
  ```

  ```python Python theme={null}
  # Drag element to target
  await page.locator("#draggable").drag_to(page.locator("#drop-zone"))

  # With precise positioning
  await page.locator("#draggable").drag_to(
      page.locator("#drop-zone"),
      source_position={"x": 10, "y": 10},
      target_position={"x": 50, "y": 50},
  )
  ```
</CodeGroup>

## Related resources

* **[Playwright basics (TypeScript)](https://github.com/intuned/cookbook/tree/main/typescript-examples/playwright-basics)** — Cookbook example with TypeScript
* **[Playwright basics (Python)](https://github.com/intuned/cookbook/tree/main/python-examples/playwright-basics)** — Cookbook example with Python
* **[Intuned SDK overview](/docs/automation-sdks/overview)** — Helper functions for common tasks
* **[Playwright official docs](https://playwright.dev/docs/intro)** — Full Playwright documentation


# Scrape without writing selectors
Source: https://intunedhq.com/docs/main/01-learn/recipes/ai-scraper



## Recipe

This recipe shows how to extract structured data from web pages using AI without writing CSS or XPath selectors. Use [`extractStructuredData`](/docs/automation-sdks/intuned-sdk/typescript/ai/functions/extractStructuredData) (TypeScript) or [`extract_structured_data`](/docs/automation-sdks/intuned-sdk/python/ai/functions/extract_structured_data) (Python).

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { BrowserContext, Page } from "playwright";
  import { extractStructuredData } from "@intuned/browser/ai";
  import { z } from "zod";

  // Define the schema for a single product
  const ProductSchema = z.object({
    name: z.string().describe("Product name"),
    price: z.string().describe("Product price"),
    stock: z.string().describe("Stock status"),
    category: z.string().describe("Product category"),
  });

  // Define the schema for the list of products
  const ProductsSchema = z.object({
    products: z.array(ProductSchema).describe("List of products from the table"),
  });

  export default async function handler(
    params: any,
    page: Page,
    context: BrowserContext
  ) {
    await page.goto("https://www.scrapingcourse.com/table-parsing");

    // Extract products using AI - no selectors needed
    const result = await extractStructuredData({
      source: page,
      dataSchema: ProductsSchema,
      prompt: "Extract all products from the table",
    });

    console.log(`Extracted ${result.products.length} products`);
    return result.products;
  }
  ```

  ```python Python theme={null}
  from playwright.async_api import Page
  from pydantic import BaseModel, Field
  from intuned_browser.ai import extract_structured_data
  from typing import Any, Dict, List


  class Product(BaseModel):
      """Schema for a single product."""
      name: str = Field(description="Product name")
      price: str = Field(description="Product price")
      stock: str = Field(description="Stock status")
      category: str = Field(description="Product category")


  class Products(BaseModel):
      """Schema for the list of products."""
      products: List[Product] = Field(description="List of products from the table")


  async def automation(page: Page, params: Dict[str, Any] = None, **_kwargs):
      await page.goto("https://www.scrapingcourse.com/table-parsing")

      # Extract products using AI - no selectors needed
      result = await extract_structured_data(
          source=page,
          data_schema=Products,
          prompt="Extract all products from the table",
      )

      print(f"Extracted {len(result['products'])} products")
      return result["products"]
  ```
</CodeGroup>

## How it works

1. **Define a schema** - Use Zod (TypeScript) or Pydantic (Python) to describe the data structure you want to extract
2. **Call the AI extractor** - Pass the page and schema to `extractStructuredData` / `extract_structured_data`
3. **Get structured data** - The AI analyzes the page and returns data matching your schema

No need to inspect the DOM, write selectors, or handle edge cases—the AI handles it all.

## Related SDK methods

<CardGroup>
  <Card title="extractStructuredData (TypeScript)" icon="js" href="/automation-sdks/intuned-sdk/typescript/ai/functions/extractStructuredData">
    TypeScript SDK helper for AI data extraction
  </Card>

  <Card title="extract_structured_data (Python)" icon="python" href="/automation-sdks/intuned-sdk/python/ai/functions/extract_structured_data">
    Python SDK helper for AI data extraction
  </Card>

  <Card title="Cookbook (Python)" icon="github" href="https://github.com/Intuned/cookbook/tree/main/python-examples/quick-recipes">
    Python quick recipes in the Intuned Cookbook
  </Card>

  <Card title="Cookbook (TypeScript)" icon="github" href="https://github.com/Intuned/cookbook/tree/main/typescript-examples/quick-recipes">
    TypeScript quick recipes in the Intuned Cookbook
  </Card>
</CardGroup>


# Capture screenshots
Source: https://intunedhq.com/docs/main/01-learn/recipes/capture-screenshots



## Recipe

This recipe shows how to capture a screenshot with Playwright's `page.screenshot()` and upload it with [`uploadFileToS3`](/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/uploadFileToS3) (TypeScript) or [`upload_file_to_s3`](/docs/automation-sdks/intuned-sdk/python/helpers/functions/upload_file_to_s3) (Python).

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { BrowserContext, Page } from "playwright";
  import { uploadFileToS3 } from "@intuned/browser"

  export default async function handler(
    params: any,
    page: Page,
    context: BrowserContext
  ) {
    await page.goto("https://www.example.com")

    // Capture screenshot as bytes
    const screenshotInBytes = await page.screenshot()

    // Upload to S3
    const uploadedFile = await uploadFileToS3({
      file: screenshotInBytes,
      fileNameOverride: "screenshot.png",
      contentType: "image/png"
    })

    // Get signed URL for access
    const signedUrl = await uploadedFile.getSignedUrl()
    console.log(
      `check here to see the screenshot: \x1b[1;4;36m\x1b]8;;${signedUrl}\x1b\\Click here\x1b]8;;\x1b\\\x1b[0m \n`
    )
    return uploadedFile
  }
  ```

  ```python Python theme={null}
  from playwright.async_api import Page
  from intuned_browser import upload_file_to_s3

  async def automation(page: Page, params=None, **_kwargs):
      await page.goto("https://www.example.com")

      # Capture screenshot as bytes
      screenshot_in_bytes = await page.screenshot()

      # Upload to S3
      uploaded_file = await upload_file_to_s3(
          screenshot_in_bytes,
          file_name_override="screenshot.png",
          content_type="image/png",
      )

      # Get signed URL for access
      signed_url = await uploaded_file.get_signed_url()
      print(
          f"Click here to see the screenshot: \033[1;4;36m\033]8;;{signed_url}\033\\Click here\033]8;;\033\\\033[0m \n"
      )
      return uploaded_file
  ```
</CodeGroup>

## Related SDK methods

<CardGroup>
  <Card title="uploadFileToS3 (TypeScript)" icon="js" href="/automation-sdks/intuned-sdk/typescript/helpers/functions/uploadFileToS3">
    TypeScript SDK helper for uploading files to S3
  </Card>

  <Card title="upload_file_to_s3 (Python)" icon="python" href="/automation-sdks/intuned-sdk/python/helpers/functions/upload_file_to_s3">
    Python SDK helper for uploading files to S3
  </Card>

  <Card title="Cookbook (Python)" icon="github" href="https://github.com/Intuned/cookbook/tree/main/python-examples/quick-recipes">
    Python quick recipes in the Intuned Cookbook
  </Card>

  <Card title="Cookbook (TypeScript)" icon="github" href="https://github.com/Intuned/cookbook/tree/main/typescript-examples/quick-recipes">
    TypeScript quick recipes in the Intuned Cookbook
  </Card>
</CardGroup>


# Crawl websites
Source: https://intunedhq.com/docs/main/01-learn/recipes/crawling



## Recipe

This recipe shows how to crawl websites and extract content as markdown using [Crawl4AI](https://crawl4ai.com/) with Intuned's browser infrastructure.

## Project structure

```plaintext theme={null}
api/
  └── simple.py          # Simple crawling example
hooks/
  └── setup_context.py   # Browser context setup
utils/
  └── config.py          # Browser configuration
pyproject.toml         # Dependencies
```

## Setup

### pyproject.toml

```toml theme={null}
[build-system]
requires = ["hatchling>=1.18.0"]
build-backend = "hatchling.build"

[project]
name = "default"
version = "0.0.1"
description = "Empty Intuned project"
readme = "README.md"
requires-python = ">=3.12,<3.13"
authors = [
  { name = "Intuned", email = "service@intunedhq.com" }
]
keywords = ["Python", "intuned-browser-sdk"]

dependencies = [
  "playwright==1.55.0",
  "intuned-runtime==1.3.10",
  "intuned-browser==0.1.9",
  "crawl4ai==0.8.6",
]


[tool.uv]
package = false
```

### hooks/setup\_context.py

Store the CDP URL so Crawl4AI can connect to Intuned's browser:

```python theme={null}
from intuned_runtime import attempt_store


async def setup_context(*, api_name: str, api_parameters: str, cdp_url: str):
    attempt_store.set("cdp_url", cdp_url)
```

### utils/config.py

Create the browser configuration for Crawl4AI using the CDP URL:

```python theme={null}
from crawl4ai import BrowserConfig
from intuned_runtime import attempt_store


def get_browser_config() -> BrowserConfig:
    cdp_url = attempt_store.get("cdp_url")

    return BrowserConfig(
        verbose=True,
        cdp_url=cdp_url,
        headless=False,
        accept_downloads=True,
    )
```

## Crawl a single page

Crawl a single page and extract its content as markdown:

```python theme={null}
from playwright.async_api import Page
from typing import TypedDict
from crawl4ai import (
    AsyncWebCrawler,
    CrawlerRunConfig,
    DefaultMarkdownGenerator,
    PruningContentFilter,
    CrawlResult,
)
from utils.config import get_browser_config


class Params(TypedDict):
    pass


async def automation(page: Page, params: Params | None = None, **_kwargs):
    browser_config = get_browser_config()
    async with AsyncWebCrawler(config=browser_config) as crawler:
        crawler_config = CrawlerRunConfig(
            markdown_generator=DefaultMarkdownGenerator(
                content_filter=PruningContentFilter(),
            ),
        )
        result: CrawlResult = await crawler.arun(
            url="https://www.helloworld.org", config=crawler_config
        )
        return result.markdown.raw_markdown
```

## Related links

<CardGroup>
  <Card title="Cookbook" icon="github" href="https://github.com/Intuned/cookbook/tree/main/python-examples/crawl4ai">
    Complete Crawl4AI example in the Intuned Cookbook
  </Card>
</CardGroup>


# Download files
Source: https://intunedhq.com/docs/main/01-learn/recipes/download-file



## Recipe

This recipe shows how to download files triggered by a button click or link with [`downloadFile`](/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/downloadFile) (TypeScript) or [`download_file`](/docs/automation-sdks/intuned-sdk/python/helpers/functions/download_file) (Python).

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { BrowserContext, Page } from "playwright";
  import { downloadFile } from "@intuned/browser"

  interface Params {
    // Add your params here
  }

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    await page.goto("https://sandbox.intuned.dev/pdfs")

    // Locate the download button
    const downloadLocator = page.locator("xpath=//tbody/tr[1]//*[name()='svg']")

    // Trigger download and wait for file
    const downloadedFile = await downloadFile({
      page,
      trigger: downloadLocator,
      timeoutInMs: 15000,
    })

    const fileName = downloadedFile.suggestedFilename()
    return fileName
  }
  ```

  ```python Python theme={null}
  from intuned_browser import download_file
  from playwright.async_api import Page

  async def automation(page: Page, params, **_kwargs):
      await page.goto("https://sandbox.intuned.dev/pdfs")

      # Locate the download button
      download_locator = page.locator("xpath=//tbody/tr[1]//*[name()='svg']")

      # Trigger download and wait for file
      downloaded_file = await download_file(
          page=page,
          trigger=download_locator,
          timeout_s=15,
      )

      file_name = downloaded_file.suggested_filename
      return file_name
  ```
</CodeGroup>

## Related SDK methods

<CardGroup>
  <Card title="downloadFile (TypeScript)" icon="js" href="/automation-sdks/intuned-sdk/typescript/helpers/functions/downloadFile">
    TypeScript SDK helper for downloading files
  </Card>

  <Card title="download_file (Python)" icon="python" href="/automation-sdks/intuned-sdk/python/helpers/functions/download_file">
    Python SDK helper for downloading files
  </Card>

  <Card title="saveFileToS3 (TypeScript)" icon="js" href="/automation-sdks/intuned-sdk/typescript/helpers/functions/saveFileToS3">
    TypeScript SDK helper for saving files to S3
  </Card>

  <Card title="save_file_to_s3 (Python)" icon="python" href="/automation-sdks/intuned-sdk/python/helpers/functions/save_file_to_s3">
    Python SDK helper for saving files to S3
  </Card>

  <Card title="Cookbook (Python)" icon="github" href="https://github.com/Intuned/cookbook/tree/main/python-examples/quick-recipes">
    Python quick recipes in the Intuned Cookbook
  </Card>

  <Card title="Cookbook (TypeScript)" icon="github" href="https://github.com/Intuned/cookbook/tree/main/typescript-examples/quick-recipes">
    TypeScript quick recipes in the Intuned Cookbook
  </Card>
</CardGroup>


# Extend API timeout
Source: https://intunedhq.com/docs/main/01-learn/recipes/extend-api-timeout

Use `extendTimeout` to keep automations running beyond the default 10-minute timeout

## Recipe

<Note>
  Long-running automations are less reliable than smaller, retryable units of work. When possible, break your automation into smaller steps that can complete within the default timeout.
</Note>

This recipe shows how to extend timeout limits for long-running automations using [`extendTimeout`](/docs/main/05-references/runtime-sdk-typescript/extend-timeout) (TypeScript) or [`extend_timeout`](/docs/main/05-references/runtime-sdk-python/extend-timeout) (Python).

This example demonstrates processing a large list with delays between requests—common when scraping rate-limited websites page by page.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { extendTimeout } from "@intuned/runtime";
  import { BrowserContext, Page } from "playwright";

  interface Book {
    title: string;
    price: string;
    url: string;
  }

  export default async function handler(
    params: { maxPages?: number },
    page: Page,
    context: BrowserContext
  ) {
    const allBooks: Book[] = [];
    const maxPages = params.maxPages || 50;
    let currentPage = 1;

    await page.goto("https://books.toscrape.com/");

    while (currentPage <= maxPages) {
      console.log(`Processing page ${currentPage} of ${maxPages}...`);

      // Extract books from current page
      const bookElements = page.locator("article.product_pod");
      const count = await bookElements.count();

      for (let i = 0; i < count; i++) {
        const book = bookElements.nth(i);
        const title = await book.locator("h3 a").getAttribute("title");
        const price = await book.locator(".price_color").textContent();
        const bookUrl = await book.locator("h3 a").getAttribute("href");

        if (title && price && bookUrl) {
          allBooks.push({
            title,
            price: price.trim(),
            url: `https://books.toscrape.com/${bookUrl}`,
          });
        }
      }

      console.log(`Completed page ${currentPage}. Books collected: ${allBooks.length}`);

      // Extend timeout after completing this unit of work
      extendTimeout();

      // Check if there's a next page
      const nextButton = page.locator(".next a");
      const hasNext = (await nextButton.count()) > 0;

      if (!hasNext || currentPage >= maxPages) {
        break;
      }

      // Add delay between page requests to respect rate limits
      await new Promise(resolve => setTimeout(resolve, 10000));

      // Navigate to next page
      await nextButton.click();
      await page.waitForLoadState("networkidle");

      currentPage++;
    }

    console.log(`Total books collected: ${allBooks.length}`);
    return allBooks;
  }
  ```

  ```python Python theme={null}
  import asyncio
  from intuned_runtime import extend_timeout
  from playwright.async_api import Page, BrowserContext
  from typing import Any, Dict, List

  async def automation(page: Page, params: Dict[str, Any] = None, **_kwargs):
      all_books = []
      max_pages = params.get("max_pages", 50) if params else 50
      current_page = 1

      await page.goto("https://books.toscrape.com/")

      while current_page <= max_pages:
          print(f"Processing page {current_page} of {max_pages}...")

          # Extract books from current page
          book_elements = page.locator("article.product_pod")
          count = await book_elements.count()

          for i in range(count):
              book = book_elements.nth(i)
              title = await book.locator("h3 a").get_attribute("title")
              price = await book.locator(".price_color").text_content()
              book_url = await book.locator("h3 a").get_attribute("href")

              if title and price and book_url:
                  all_books.append({
                      "title": title,
                      "price": price.strip(),
                      "url": f"https://books.toscrape.com/{book_url}",
                  })

          print(f"Completed page {current_page}. Books collected: {len(all_books)}")

          # Extend timeout after completing this unit of work
          extend_timeout()

          # Check if there's a next page
          next_button = page.locator(".next a")
          has_next = await next_button.count() > 0

          if not has_next or current_page >= max_pages:
              break

          # Add delay between page requests to respect rate limits
          await asyncio.sleep(10)

          # Navigate to next page
          await next_button.click()
          await page.wait_for_load_state("networkidle")

          current_page += 1

      print(f"Total books collected: {len(all_books)}")
      return all_books
  ```
</CodeGroup>

## Understanding timeouts

1. **Default timeout prevents stuck Runs** — Each Run attempt times out after 600 seconds (10 minutes) by default, configurable via `requestTimeout`. This prevents stuck automations from wasting resources.

2. **extendTimeout() resets the timer** — Each call resets the countdown to the original `requestTimeout` value. Call it as many times as needed, up to a 6-hour maximum total duration per Run.

3. **Correct placement is critical** — Call `extendTimeout()` after completing each unit of work (a page, batch, or logical section). Each unit must finish within the original timeout. For automations requiring more than 6 hours, contact Intuned support.

## Related SDK methods

<CardGroup>
  <Card title="extendTimeout (TypeScript)" icon="js" href="/main/05-references/runtime-sdk-typescript/extend-timeout">
    Reset the timeout in TypeScript
  </Card>

  <Card title="extend_timeout (Python)" icon="python" href="/main/05-references/runtime-sdk-python/extend-timeout">
    Reset the timeout in Python
  </Card>

  <Card title="Jobs configuration" icon="clock" href="/main/02-features/jobs-batched-executions">
    Configure requestTimeout and retry settings
  </Card>
</CardGroup>


# Expand JobRun payload dynamically
Source: https://intunedhq.com/docs/main/01-learn/recipes/extend-payload



## Recipe

This recipe shows how to chain API executions by extending a job's payload within your code using Intuned's [`extendPayload`](/docs/main/05-references/runtime-sdk-typescript/extend-payload) (TypeScript) or [`extend_payload`](/docs/main/05-references/runtime-sdk-python/extend-payload) (Python) helper.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { extendPayload } from "@intuned/runtime";
  import { BrowserContext, Page } from "playwright";

  export default async function handler(
    params: any,
    page: Page,
    context: BrowserContext
  ) {
    await page.goto("https://books.toscrape.com/");

    const bookTitles: { title: string }[] = [];

    // Scraping book titles
    const bookLocators = page.locator("ol li");
    const count = await bookLocators.count();

    const allLocators = await bookLocators.all();

    for (const bookLocator of allLocators) {
      const titleLocator = bookLocator.locator("h3");
      const txt = (await titleLocator.textContent())?.trim();

      const detailsUrlLocator = bookLocator.locator("div.image_container a");
      const href = await detailsUrlLocator.getAttribute("href");
      const detailsUrl = "https://books.toscrape.com/" + href;

      if (txt) {
        bookTitles.push({ title: txt });
       
      }
      // Extending payload
      extendPayload({
          api: "details",
          parameters: { url: detailsUrl },
        });
    }
  }
  ```

  ```python Python theme={null}
  from intuned_runtime import extend_payload
  from playwright.async_api import Page, BrowserContext

  async def automation(page: Page, params: dict | None = None, **_kwargs):
      await page.goto("https://books.toscrape.com/")

      book_titles = []

      # Scraping book titles
      book_locators = page.locator("ol li")
      count = await book_locators.count()

      all_locators = await book_locators.all()

      for book_locator in all_locators:
          title_locator = book_locator.locator("h3")
          txt = await title_locator.text_content()
          txt = txt.strip() if txt else None

          details_locator = book_locator.locator("div.image_container a")
          href = await details_locator.get_attribute("href")
          details_url = f"https://books.toscrape.com/{href}"

          if txt:
              book_titles.append({"title": txt})

          # Extending payload
          extend_payload({
              "api": "details",
              "parameters": {"url": details_url},
          })

  ```
</CodeGroup>

## Related SDK methods

<CardGroup>
  <Card title="extendPayload (TypeScript)" icon="js" href="/main/05-references/runtime-sdk-typescript/extend-payload">
    TypeScript SDK helper for extending payloads
  </Card>

  <Card title="extend_payload (Python)" icon="python" href="/main/05-references/runtime-sdk-python/extend-payload">
    Python SDK helper for extending payloads
  </Card>

  <Card title="nested_scheduling" href="/main/02-features/jobs-batched-executions#nested-scheduling">
    Nested scheduling
  </Card>
</CardGroup>


# Handle infinite scrolling
Source: https://intunedhq.com/docs/main/01-learn/recipes/infinite-scrolling



## Recipe

This recipe shows how to scrape data from infinite scroll pages using [`scrollToLoadContent`](/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/scrollToLoadContent) (TypeScript) or [`scroll_to_load_content`](/docs/automation-sdks/intuned-sdk/python/helpers/functions/scroll_to_load_content) (Python).

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { BrowserContext, Page } from "playwright";
  import { scrollToLoadContent } from "@intuned/browser";

  interface Product {
    name: string;
    price: string;
  }

  // Extract products from the page
  async function extractProducts(page: Page): Promise<Product[]> {
    const results: Product[] = [];
    const productCards = page.locator(".product-item");
    const count = await productCards.count();

    for (let i = 0; i < count; i++) {
      const card = productCards.nth(i);
      const name = await card.locator(".product-name").textContent();
      const price = await card.locator(".product-price").textContent();

      if (name && price) {
        results.push({
          name: name.trim(),
          price: price.trim(),
        });
      }
    }

    return results;
  }

  export default async function handler(
    params: { maxScrolls?: number },
    page: Page,
    context: BrowserContext
  ) {
    await page.goto("https://www.scrapingcourse.com/infinite-scrolling");

    // Scroll to load all content
    await scrollToLoadContent({
      source: page,
      maxScrolls: params.maxScrolls ?? 50,
    });

    // Extract all products after content is loaded
    const products = await extractProducts(page);
    console.log(`Extracted ${products.length} products`);

    return products;
  }
  ```

  ```python Python theme={null}
  from playwright.async_api import Page
  from intuned_browser import scroll_to_load_content
  from typing import Any, Dict, List, Optional


  async def extract_products(page: Page) -> List[Dict[str, str]]:
      """Extract products from the page."""
      results = []
      product_cards = page.locator(".product-item")
      count = await product_cards.count()

      for i in range(count):
          card = product_cards.nth(i)
          name = await card.locator(".product-name").text_content()
          price = await card.locator(".product-price").text_content()

          if name and price:
              results.append({
                  "name": name.strip(),
                  "price": price.strip(),
              })

      return results


  async def automation(page: Page, params: Optional[Dict] = None, **_kwargs):
      await page.goto("https://www.scrapingcourse.com/infinite-scrolling")

      max_scrolls = params.get("max_scrolls", 50) if params else 50

      # Scroll to load all content
      await scroll_to_load_content(
          source=page,
          max_scrolls=max_scrolls,
      )

      # Extract all products after content is loaded
      products = await extract_products(page)
      print(f"Extracted {len(products)} products")

      return products
  ```
</CodeGroup>

## Related SDK methods

<CardGroup>
  <Card title="scrollToLoadContent (TypeScript)" icon="js" href="/automation-sdks/intuned-sdk/typescript/helpers/functions/scrollToLoadContent">
    TypeScript SDK helper for scrolling to load content
  </Card>

  <Card title="scroll_to_load_content (Python)" icon="python" href="/automation-sdks/intuned-sdk/python/helpers/functions/scroll_to_load_content">
    Python SDK helper for scrolling to load content
  </Card>

  <Card title="Cookbook (Python)" icon="github" href="https://github.com/Intuned/cookbook/tree/main/python-examples/quick-recipes">
    Python quick recipes in the Intuned Cookbook
  </Card>

  <Card title="Cookbook (TypeScript)" icon="github" href="https://github.com/Intuned/cookbook/tree/main/typescript-examples/quick-recipes">
    TypeScript quick recipes in the Intuned Cookbook
  </Card>
</CardGroup>


# Persist data with KV cache
Source: https://intunedhq.com/docs/main/01-learn/recipes/kv-cache



## Recipe

This recipe shows how to use [`persistentStore`](/docs/main/05-references/runtime-sdk-typescript/persistent-store) (TypeScript) or [`persistent_store`](/docs/main/05-references/runtime-sdk-python/persistent-store) (Python) to cache scraped data across executions of the same project.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { BrowserContext, Page } from "playwright";
  import { persistentStore } from "@intuned/runtime";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    const CACHE_KEY = "consultations";

    // 1. Check cache first
    const cached = await persistentStore.get(CACHE_KEY);
    if (cached) {
      return cached;
    }

    // 2. Cache miss — navigate and scrape the page
    await page.goto("https://sandbox.intuned.dev/consultations/list");
    await page.waitForSelector(".consultation-item");

    const consultations = await page.locator(".consultation-item").evaluateAll(
      (items) =>
        items.map((item) => ({
          id: item.getAttribute("data-consultation-id"),
          status: item.querySelector(".consultation-status")?.textContent?.trim(),
          name: item.querySelector(".client-name .data-value")?.textContent?.trim(),
          date: item.querySelector(".consultation-date .data-value")?.textContent?.trim(),
          topic: item.querySelector(".consultation-topic .data-value")?.textContent?.trim(),
        }))
    );

    // 3. Store for future runs
    await persistentStore.set(CACHE_KEY, consultations);

    return consultations;
  }
  ```

  ```python Python theme={null}
  from playwright.async_api import Page
  from intuned_runtime import persistent_store
  from typing import Any, Dict


  async def automation(page: Page, params: Dict[str, Any] | None = None, **_kwargs):
      CACHE_KEY = "consultations"

      # 1. Check cache first
      cached = await persistent_store.get(CACHE_KEY)
      if cached:
          return cached

      # 2. Cache miss — navigate and scrape the page
      await page.goto("https://sandbox.intuned.dev/consultations/list")
      await page.wait_for_selector(".consultation-item")

      items = await page.locator(".consultation-item").all()
      consultations = []
      for item in items:
          consultations.append({
              "id": await item.get_attribute("data-consultation-id"),
              "status": await item.locator(".consultation-status").inner_text(),
              "name": await item.locator(".client-name .data-value").inner_text(),
              "date": await item.locator(".consultation-date .data-value").inner_text(),
              "topic": await item.locator(".consultation-topic .data-value").inner_text(),
          })

      # 3. Store for future runs
      await persistent_store.set(CACHE_KEY, consultations)

      return consultations
  ```
</CodeGroup>

## How it works

1. **Check the cache** — Use `persistentStore.get()` / `persistent_store.get()` to look for existing data.
2. **Return cached data** — If found, return immediately without re-scraping.
3. **Scrape and store** — On cache miss, navigate to the page, extract data, and store it with `persistentStore.set()` / `persistent_store.set()` for future runs.

Data persists across all executions within the same project, making it ideal for data that doesn't change frequently.

## Related SDK methods

<CardGroup>
  <Card title="persistentStore (TypeScript)" icon="js" href="/main/05-references/runtime-sdk-typescript/persistent-store">
    TypeScript SDK helper for persistent key-value storage
  </Card>

  <Card title="persistent_store (Python)" icon="python" href="/main/05-references/runtime-sdk-python/persistent-store">
    Python SDK helper for persistent key-value storage
  </Card>
</CardGroup>


# Handle load more buttons
Source: https://intunedhq.com/docs/main/01-learn/recipes/load-more-button



## Recipe

This recipe shows how to scrape data from pages with "Load More" buttons by clicking until the button disappears or reaches a maximum number of clicks.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { BrowserContext, Page, Locator } from "playwright";

  interface BoxItem {
    text: string;
  }

  // Click button until it disappears or max clicks reached
  async function clickUntilExhausted(
    buttonLocator: Locator,
    maxClicks: number,
    page:Page
  ): Promise<void> {
    let clicks = 0;
    while (clicks < maxClicks) {
      await page.waitForTimeout(3000)
      const isVisible = await buttonLocator.isVisible();
      if (!isVisible) {
        break;
      }
      await buttonLocator.click();
      clicks++;
    }
  }

  // Extract box items from the page
  async function extractItems(page: Page): Promise<BoxItem[]> {
    const results: BoxItem[] = [];
    const boxes = page.locator("div.grid div.h-64");
    const count = await boxes.count();

    for (let i = 0; i < count; i++) {
      const box = boxes.nth(i);
      const text = await box.textContent();
      if (text) {
        results.push({
          text: text.trim(),
        });
      }
    }
    return results;
  }

  export default async function handler(
    params: { maxClicks?: number },
    page: Page,
    context: BrowserContext
  ) {
    await page.goto("https://sandbox.intuned.dev/load-more");

    // Locate the "Load More" button in main content area (not sidebar)
    const loadMoreButton = page.locator("main.flex-1 button:has-text('Load More')");

    // Click until button disappears or max clicks reached
    await clickUntilExhausted(loadMoreButton, params.maxClicks ?? 50,page);

    // Extract all items after content is loaded
    const items = await extractItems(page);
    console.log(`Extracted ${items.length} items`);

    return items;
  }
  ```

  ```python Python theme={null}
  from playwright.async_api import Page, Locator
  from typing import Any, Dict, List, Optional


  async def click_until_exhausted(button_locator: Locator, max_clicks: int,page:Page) -> None:
      """Click button until it disappears or max clicks reached."""
      clicks = 0

      while clicks < max_clicks:
          await page.wait_for_timeout(3000)
          is_visible = await button_locator.is_visible()
          if not is_visible:
              break

          await button_locator.click()
          clicks += 1


  async def extract_items(page: Page) -> List[str]:
      """Extract box items from the page."""
      items = []
      boxes = page.locator("div.grid div.h-64")
      count = await boxes.count()

      for i in range(count):
          box = boxes.nth(i)
          text = await box.text_content()
          if text:
              items.append(text.strip())

      return items


  async def automation(page: Page, params: Optional[Dict] = None, **_kwargs):
      await page.goto("https://sandbox.intuned.dev/load-more")

      # Wait for page to load
      await page.wait_for_load_state("networkidle")

      max_clicks = params.get("max_clicks", 50) if params else 50

      # Locate the "Load More" button
      load_more_button = page.locator("main.flex-1 button:has-text('Load More')")

      # Click until button disappears or max clicks reached
      await click_until_exhausted(load_more_button, max_clicks,page)

      # Extract all items after content is loaded
      items = await extract_items(page)
      print(f"Extracted {len(items)} items")

      return items
  ```
</CodeGroup>

## Related links

<CardGroup>
  <Card title="Cookbook (Python)" icon="github" href="https://github.com/Intuned/cookbook/tree/main/python-examples/quick-recipes">
    Python quick recipes in the Intuned Cookbook
  </Card>

  <Card title="Cookbook (TypeScript)" icon="github" href="https://github.com/Intuned/cookbook/tree/main/typescript-examples/quick-recipes">
    TypeScript quick recipes in the Intuned Cookbook
  </Card>
</CardGroup>


# Intercept network requests
Source: https://intunedhq.com/docs/main/01-learn/recipes/network-interception



## Recipe

This recipe shows how to extract data directly from API responses by intercepting network requests. Use Playwright's response listener with [`withNetworkSettledWait`](/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/withNetworkSettledWait) (TypeScript) or [`wait_for_network_settled`](/docs/automation-sdks/intuned-sdk/python/helpers/functions/wait_for_network_settled) (Python).

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { BrowserContext, Page, Response } from "playwright";
  import { withNetworkSettledWait } from "@intuned/browser";

  let apiData: any[] = [];

  async function handleResponse(response: Response): Promise<void> {
    if (response.url().includes("/rest/v1/consultations")) {
      try {
        apiData = await response.json();
      } catch (e) {
        // Response might not be JSON
      }
    }
  }

  export default async function handler(
    params: any,
    page: Page,
    context: BrowserContext
  ) {
    apiData = [];

    // Listen for responses matching the pattern
    page.on("response", handleResponse);

    // Wait until the page is fully loaded and in the idle state
    await withNetworkSettledWait(
      async (page) => {
        await page.goto('https://sandbox.intuned.dev/consultations/list');
      },
      {
        page,
        timeoutInMs: 20000,
      }
    );
    console.log(`Captured ${apiData.length} consultations from API`);
    return apiData;
  }
  ```

  ```python Python theme={null}
  from playwright.async_api import Page, Response
  from typing import Any, Dict, List
  from intuned_browser import wait_for_network_settled


  api_data: List[Dict[str, Any]] = []


  async def handle_response(response: Response) -> None:
      if "/rest/v1/consultations" in response.url:
          try:
              data = await response.json()
              api_data.clear()
              api_data.extend(data)
          except Exception:
              # Response might not be JSON
              pass


  async def automation(page: Page, params: Dict[str, Any], **_kwargs):
      api_data.clear()

      # Listen for responses matching the pattern
      page.on("response", handle_response)

      # Navigate and wait until the network is settled
      await wait_for_network_settled(
          page=page,
          func=lambda: page.goto("https://sandbox.intuned.dev/consultations/list"),
          timeout_s=20,
      )

      print(f"Captured {len(api_data)} consultations from API")
      return api_data
  ```
</CodeGroup>

## How it works

1. **Set up response listener** — Use `page.on("response", handler)` to listen for network responses.
2. **Filter by URL** — Check if the response URL matches your target API endpoint.
3. **Capture the data** — Parse the JSON response and store it.
4. **Navigate to the page** — The listener captures API calls triggered during page load.

## Related SDK methods

<CardGroup>
  <Card title="withNetworkSettledWait (TypeScript)" icon="js" href="/automation-sdks/intuned-sdk/typescript/helpers/functions/withNetworkSettledWait">
    Wait for network to settle (TypeScript)
  </Card>

  <Card title="wait_for_network_settled (Python)" icon="python" href="/automation-sdks/intuned-sdk/python/helpers/functions/wait_for_network_settled">
    Wait for network to settle (Python)
  </Card>

  <Card title="Cookbook (Python)" icon="github" href="https://github.com/Intuned/cookbook/tree/main/python-examples/network-interception">
    Python network interception example in the Intuned Cookbook
  </Card>

  <Card title="Cookbook (TypeScript)" icon="github" href="https://github.com/Intuned/cookbook/tree/main/typescript-examples/network-interception">
    TypeScript network interception example in the Intuned Cookbook
  </Card>
</CardGroup>


# Handle pagination
Source: https://intunedhq.com/docs/main/01-learn/recipes/pagination



## Recipe

This recipe shows how to scrape data across multiple pages by iterating through pagination links using Playwright's navigation and locator methods.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { goToUrl, withNetworkSettledWait } from "@intuned/browser";
  import { BrowserContext, Page } from "playwright-core";

  interface Params {
    maxPages: number;
  }

  interface Product {
    name: string;
    price: string;
  }

  // Extract products from the current page
  async function extractDataFromCurrentPage(page: Page): Promise<Product[]> {
    const results: Product[] = [];
    const productCards = page.locator(".product-item");
    const count = await productCards.count();

    for (let i = 0; i < count; i++) {
      const card = productCards.nth(i);
      const name = await card.locator(".product-name").textContent();
      const price = await card.locator(".product-price").textContent();

      if (name && price) {
        results.push({
          name: name.trim(),
          price: price.trim(),
        });
      }
    }

    return results;
  }

  // Check if next page exists
  async function hasNextPage(page: Page): Promise<boolean> {
    const nextButton = page.locator('a:has-text("Next page")');
    return (await nextButton.count()) > 0;
  }

  // Navigate to the next page

  async function goToNextPage(page: Page): Promise<void> {
    const nextButton = page.locator('a:has-text("Next page")');
    await withNetworkSettledWait(
      async (page) => {
        await nextButton.click();
      },
      {
        page,
        timeoutInMs: 10000,
        maxInflightRequests: 0,
      }
    );
  }

  export default async function handler(
    params: Params | null,
    page: Page,
    context: BrowserContext
  ) {
    const maxPages = params?.maxPages ?? 5;
    await goToUrl({ page, url: "https://www.scrapingcourse.com/pagination" });

    const allProducts: Product[] = [];
    let currentPage = 0;

    while (currentPage < maxPages) {
      // Extract data from current page
      const results = await extractDataFromCurrentPage(page);
      console.log(
        `Extracted ${results.length} results from page ${currentPage + 1}`
      );
      allProducts.push(...results);

      // Check if there's a next page
      const canContinue = await hasNextPage(page);
      if (!canContinue) {
        console.log("No more pages available.");
        break;
      }

      currentPage++;
      if (currentPage >= maxPages) {
        break;
      }

      // Navigate to next page
      await goToNextPage(page);
    }

    return allProducts;
  }
  ```

  ```python Python theme={null}
  from playwright.async_api import Page
  from typing import List, TypedDict
  from intuned_browser import go_to_url, wait_for_network_settled


  class Params(TypedDict):
      max_pages: int


  class Product(TypedDict):
      name: str
      price: str


  async def extract_data_from_current_page(page: Page) -> List[Product]:
      """Extract products from the current page."""
      results = []
      product_cards = page.locator(".product-item")
      count = await product_cards.count()

      for i in range(count):
          card = product_cards.nth(i)
          name = await card.locator(".product-name").text_content()
          price = await card.locator(".product-price").text_content()

          if name and price:
              results.append(
                  {
                      "name": name.strip(),
                      "price": price.strip(),
                  }
              )

      return results


  async def has_next_page(page: Page) -> bool:
      """Check if next page exists."""
      next_button = page.locator('a:has-text("Next page")')
      return await next_button.count() > 0


  @wait_for_network_settled()
  async def go_to_next_page(page: Page) -> None:
      """Navigate to the next page."""
      next_button = page.locator('a:has-text("Next page")')
      await next_button.click()


  async def automation(page: Page, params: Params | None = None, **_kwargs):
      max_pages = params.get("max_pages", 5) if params else 5
      await go_to_url(page=page, url="https://www.scrapingcourse.com/pagination")

      all_products = []
      current_page = 0

      while current_page < max_pages:
          # Extract data from current page
          results = await extract_data_from_current_page(page)
          print(f"Extracted {len(results)} results from page {current_page + 1}")
          all_products.extend(results)

          # Check if there's a next page
          can_continue = await has_next_page(page)
          if not can_continue:
              print("No more pages available.")
              break

          current_page += 1
          if current_page >= max_pages:
              break

          # Navigate to next page
          await go_to_next_page(page)

      return all_products
  ```
</CodeGroup>

## Related links

<CardGroup>
  <Card title="Cookbook (Python)" icon="github" href="https://github.com/Intuned/cookbook/tree/main/python-examples/quick-recipes">
    Python quick recipes in the Intuned Cookbook
  </Card>

  <Card title="Cookbook (TypeScript)" icon="github" href="https://github.com/Intuned/cookbook/tree/main/typescript-examples/quick-recipes">
    TypeScript quick recipes in the Intuned Cookbook
  </Card>
</CardGroup>


# Validate data with schemas
Source: https://intunedhq.com/docs/main/01-learn/recipes/schema-validation



## Recipe

This recipe shows how to validate data structures using [Zod](https://zod.dev) (TypeScript) or [Pydantic](https://docs.pydantic.dev) (Python), including synchronous and asynchronous validation patterns.

<Info>
  **TypeScript:** Zod version `3.22+` is supported, which provides the `zod/v3`
  export path required by libraries like json-schema-to-zod. **Python:** Examples
  target Pydantic v2 (`pydantic>=2.0`).
</Info>

## Code example

### Synchronous validation

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { z } from "zod";

  // Define the Contract schema
  const ContractSchema = z.object({
    contractId: z.string().uuid("Invalid contract ID format"),
    clientName: z.string().min(1, "Client name is required"),
    contractValue: z.number().min(0, "Contract value must be non-negative"),
    status: z.union([
      z.literal("active"),
      z.literal("completed"),
      z.literal("terminated"),
    ]),
    startDate: z.string().datetime("Invalid start date format"),
    endDate: z.string().datetime("Invalid end date format").optional(),
  });

  // Infer TypeScript type from schema
  type Contract = z.infer<typeof ContractSchema>;

  // Example data
  const validContract = {
    contractId: "550e8400-e29b-41d4-a716-446655440000",
    clientName: "Client X",
    contractValue: 50000,
    status: "active" as const,
    startDate: "2024-01-01T00:00:00Z",
    endDate: "2024-12-31T23:59:59Z",
  };

  const invalidContract = {
    contractId: "invalid-uuid",
    clientName: "",
    contractValue: -1000, // invalid: negative value
    status: "active" as const,
    startDate: "2024-01-01T00:00:00Z",
  };

  // Synchronous validation with safeParse()

  const result1 = ContractSchema.safeParse(validContract);
  if (!result1.success) {
    console.error("Validation errors:", result1.error.format());
  } else {
    console.log("✓ Valid contract:", result1.data);
  }

  // Handling validation errors
  const result2 = ContractSchema.safeParse(invalidContract);
  if (!result2.success) {
    console.error("Validation errors:", result2.error.format());
  } else {
    console.log("Valid contract:", result2.data);
  }

  // Alternative: parse() method (throws on error)
  try {
    const validated = ContractSchema.parse(validContract);
    console.log("\n✓ Parsed successfully:", validated);
  } catch (error) {
    if (error instanceof z.ZodError) {
      console.error("Validation failed:", error.issues);
    }
  }
  ```

  ```python Python theme={null}
  from pydantic import BaseModel, Field, ValidationError
  from typing import Literal, Optional
  from datetime import datetime


  # Define the Contract model
  class Contract(BaseModel):
      contract_id: str
      client_name: str = Field(min_length=1, description="Client name is required")
      contract_value: float = Field(ge=0, description="Must be non-negative")
      status: Literal["active", "completed", "terminated"]
      start_date: datetime
      end_date: Optional[datetime] = None


  # Example data
  valid_contract = {
      "contract_id": "550e8400",
      "client_name": "Client X",
      "contract_value": 50000,
      "status": "active",
      "start_date": "2024-01-01T00:00:00Z",
      "end_date": "2024-12-31T23:59:59Z",
  }

  invalid_contract = {
      "contract_id": "invalid-uuid",
      "client_name": "",
      "contract_value": -1000,  # invalid: negative value
      "status": "active",
      "start_date": "2024-01-01T00:00:00Z",
  }

  # Validate with model_validate() — raises ValidationError on failure
  try:
      contract = Contract.model_validate(valid_contract)
      print("✓ Valid contract:", contract)
  except ValidationError as e:
      print("Validation errors:", e.errors())

  # Handling validation errors
  try:
      contract = Contract.model_validate(invalid_contract)
  except ValidationError as e:
      for error in e.errors():
          print(f"  - {error['loc']}: {error['msg']}")

  # Alternative: model_construct() skips validation (use only for trusted data)
  trusted = Contract.model_construct(**valid_contract)
  print("\n✓ Constructed without validation:", trusted)
  ```
</CodeGroup>

### Asynchronous validation

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { z } from "zod";

  const asyncContract = {
    contractId: "550e8400-e29b-41d4-a716-446655440000",
    clientName: "Client Y",
    contractValue: 75000,
    status: "active" as const,
    startDate: "2024-06-01T00:00:00Z",
  };

  // Asynchronous validation with refinements

  const AsyncContractSchema = ContractSchema.extend({
    clientName: z.string().refine(
      async (val) => {
        // Simulate database check for duplicate client
        await new Promise((resolve) => setTimeout(resolve, 100));
        return val !== "Client X"; // Fail if client already has active contract
      },
      { message: "Client already has an active contract" }
    ),
  });

  AsyncContractSchema.safeParseAsync(asyncContract)
    .then((result) => {
      if (!result.success) {
        console.error("✗ Async validation errors:", result.error.format());
      } else {
        console.log("✓ Valid contract (async):", result.data);
      }
    })
    .catch((err) => console.error("Unexpected error:", err));
  ```

  ```python Python theme={null}
  from pydantic import BaseModel, Field, ValidationError
  from typing import Literal, Optional
  from datetime import datetime
  import asyncio


  class Contract(BaseModel):
      contract_id: str
      client_name: str = Field(min_length=1)
      contract_value: float = Field(ge=0)
      status: Literal["active", "completed", "terminated"]
      start_date: datetime
      end_date: Optional[datetime] = None


  async def check_no_duplicate_client(client_name: str) -> bool:
      """Simulate a database check for duplicate clients."""
      await asyncio.sleep(0.1)
      return client_name != "Client X"  # Fail if client already has active contract


  async def validate_contract_async(data: dict) -> Contract:
      # Step 1: structural validation (Pydantic validators are synchronous)
      contract = Contract.model_validate(data)

      # Step 2: async business logic checks after structural validation passes
      if not await check_no_duplicate_client(contract.client_name):
          raise ValueError("Client already has an active contract")

      return contract


  async def main():
      async_contract = {
          "contract_id": "660e8400",
          "client_name": "Client Y",
          "contract_value": 75000,
          "status": "active",
          "start_date": "2024-06-01T00:00:00Z",
      }

      try:
          contract = await validate_contract_async(async_contract)
          print("✓ Valid contract (async):", contract)
      except (ValidationError, ValueError) as e:
          print("✗ Validation error:", e)


  asyncio.run(main())
  ```
</CodeGroup>

<Info>
  Pydantic validators run synchronously. For async checks (like database lookups), validate the data structure first with `model_validate()`, then run async logic afterward—as shown above.
</Info>

## Comparison: TypeScript vs Python

| Pattern                    | TypeScript (Zod)                           | Python (Pydantic)                             |
| -------------------------- | ------------------------------------------ | --------------------------------------------- |
| Safe validation (no throw) | `safeParse()` → `{ success, data, error }` | `try/except ValidationError`                  |
| Strict validation (throws) | `parse()` → throws `ZodError`              | `model_validate()` → raises `ValidationError` |
| Async validation           | `.safeParseAsync()` / `.refine(async fn)`  | Validate sync first, then run async checks    |
| Skip validation            | N/A                                        | `model_construct()`                           |
| Access errors              | `error.format()` / `error.issues`          | `e.errors()` → list of dicts                  |

## Common pitfalls

**Date parsing** — Pydantic automatically parses ISO 8601 strings into `datetime` objects. Watch out for timezone-aware vs. naive datetimes: `"2024-01-01T00:00:00Z"` becomes timezone-aware, while `"2024-01-01T00:00:00"` is naive. Mixing them raises an error.

**Optional fields** — Mark optional fields with `Optional[X] = None` (or `X | None = None` in Python 3.10+). A field typed as `Optional[X]` without a default is still required on input.

**Custom validators** — Use `@field_validator` for single-field checks and `@model_validator(mode='after')` for cross-field checks:

```python theme={null}
from pydantic import BaseModel, field_validator, model_validator


class Contract(BaseModel):
    client_name: str
    start_date: datetime
    end_date: Optional[datetime] = None

    @field_validator("client_name")
    @classmethod
    def name_must_not_be_blank(cls, v: str) -> str:
        if not v.strip():
            raise ValueError("Client name cannot be blank")
        return v.strip()

    @model_validator(mode="after")
    def end_after_start(self) -> "Contract":
        if self.end_date and self.end_date <= self.start_date:
            raise ValueError("end_date must be after start_date")
        return self
```

## Related

<CardGroup>
  <Card title="TypeScript SDK" href="/automation-sdks/overview">
    Learn about the Intuned Browser SDK for TypeScript.
  </Card>

  <Card title="Python SDK" href="/automation-sdks/intuned-sdk/python/overview">
    Learn about the Intuned Browser SDK for Python.
  </Card>
</CardGroup>


# Set up hooks
Source: https://intunedhq.com/docs/main/01-learn/recipes/setup-hooks



## Recipe

This recipe shows how to initialize a shared browser session in a hook and reuse it across automation steps using `attempt_store`.

## Project structure

```plaintext theme={null}
api/
  └── automation.py
hooks/
  └── setup_context.py
```

## Setup

### hooks/setup\_context.py

Connect to Intuned's browser via CDP and store the instance for later steps:

```python theme={null}
import os
from intuned_runtime import attempt_store


async def setup_context(*, api_name: str, api_parameters: str, cdp_url: str):
    os.environ["BROWSER_USE_CONFIG_DIR"] = "/tmp/browseruse-config"
    from browser_use import Browser

    browser = Browser(cdp_url=cdp_url)
    attempt_store.set("browser", browser)
```

## Use the browser in your automation

Retrieve the stored browser instance and pass it to a Browser Use agent:

```python theme={null}
from playwright.async_api import Page
from browser_use import Agent, ChatOpenAI, Browser
from intuned_runtime import attempt_store


async def automation(page: Page, params=None, **_kwargs):
    browser: Browser = attempt_store.get("browser")

    agent = Agent(
        browser=browser,
        task="Your automation task here",
        llm=ChatOpenAI(model="gpt-5.4-mini"),
    )
    await agent.run()
```

## Related links

<CardGroup>
  <Card title="Cookbook (Python)" icon="github" href="https://github.com/Intuned/cookbook/tree/main/python-examples/setup-hooks">
    Python setup hooks example in the Intuned Cookbook
  </Card>

  <Card title="Cookbook (TypeScript)" icon="github" href="https://github.com/Intuned/cookbook/tree/main/typescript-examples/setup-hooks">
    TypeScript setup hooks example in the Intuned Cookbook
  </Card>

  <Card title="attempt_store (Python)" icon="python" href="/main/05-references/runtime-sdk-python/attempt-store">
    Store and retrieve data within an attempt
  </Card>
</CardGroup>


# Set up two-factor authentication
Source: https://intunedhq.com/docs/main/01-learn/recipes/two-FA



## Recipe

This recipe shows how to set up a two-factor authentication (2FA) flow in Intuned online IDE.

## Prerequisites

<Steps>
  <Step title="Enable auth session in `intuned.json`">
    Make sure authentication sessions are enabled.
  </Step>

  <Step title="Install the two-factor authentication package">
    Add the required dependency to your project:
  </Step>
</Steps>

## Code implementation

Below is a full example of how to automate a create auth session api to login using Intuned + Playwright + TOTP.

## Related links

<CardGroup>
  <Card title="Auth sessions" href="/main/02-features/auth-sessions">
    Intuned auth sessions
  </Card>

  <Card title="Cookbook (Python)" icon="github" href="https://github.com/Intuned/cookbook/tree/main/python-examples/auth-with-secret-otp">
    Python TOTP authentication example in the Intuned Cookbook
  </Card>

  <Card title="Cookbook (TypeScript)" icon="github" href="https://github.com/Intuned/cookbook/tree/main/typescript-examples/auth-with-secret-otp">
    TypeScript TOTP authentication example in the Intuned Cookbook
  </Card>
</CardGroup>


# Upload files to S3
Source: https://intunedhq.com/docs/main/01-learn/recipes/upload-files



## Recipe

This recipe shows how to upload files to S3 with [`uploadFileToS3`](/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/uploadFileToS3) (TypeScript) or [`upload_file_to_s3`](/docs/automation-sdks/intuned-sdk/python/helpers/functions/upload_file_to_s3) (Python).

## Using uploadFileToS3 / upload\_file\_to\_s3

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { BrowserContext, Page } from "playwright";
  import { uploadFileToS3 } from "@intuned/browser"

  export default async function handler(
    params: any,
    page: Page,
    context: BrowserContext
  ) {
    await page.goto("https://sandbox.intuned.dev/pdfs")

    // Wait for download event and click the trigger
    const [download] = await Promise.all([
      page.waitForEvent("download"),
      page.locator("xpath=//tbody/tr[1]//*[name()='svg']").click(),
    ])

    // Upload the Playwright Download object to S3
    const uploadedFile = await uploadFileToS3({
      file: download,
      fileNameOverride: "myfile.pdf",
    })

    // Get signed URL for access
    const signedUrl = await uploadedFile.getSignedUrl()
    console.log(
      `Download uploaded file by clicking on: \x1b[1;4;36m\x1b]8;;${signedUrl}\x1b\\Click here\x1b]8;;\x1b\\\x1b[0m \n`
    )
    return uploadedFile
  }
  ```

  ```python Python theme={null}
  from intuned_browser import upload_file_to_s3
  from playwright.async_api import Page

  async def automation(page: Page, params, **_kwargs):
      await page.goto("https://sandbox.intuned.dev/pdfs")

      # Wait for download event and click the trigger
      async with page.expect_download() as download_info:
          await page.locator("xpath=//tbody/tr[1]//*[name()='svg']").click()
      download = await download_info.value

      # Upload the Playwright Download object to S3
      uploaded_file = await upload_file_to_s3(
          download,
          file_name_override="myfile.pdf",
          content_type="application/pdf",
      )

      # Get signed URL for access
      signed_url = await uploaded_file.get_signed_url()
      print(
          f"Download uploaded file by clicking on: \033[1;4;36m\033]8;;{signed_url}\033\\Click here\033]8;;\033\\\033[0m \n"
      )
      return uploaded_file
  ```
</CodeGroup>

## Using saveFileToS3 / save\_file\_to\_s3

[`saveFileToS3`](/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/saveFileToS3) (TypeScript) or [`save_file_to_s3`](/docs/automation-sdks/intuned-sdk/python/helpers/functions/save_file_to_s3) (Python) combines downloading and uploading in one step.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { BrowserContext, Page } from "playwright";
  import { saveFileToS3 } from "@intuned/browser"

  interface Params {
    // Add your params here
  }

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    await page.goto("https://sandbox.intuned.dev/pdfs")

    // Locate the download button
    const downloadLocator = page.locator("xpath=//tbody/tr[1]//*[name()='svg']")

    // Download and upload to S3 in one step
    const uploadedFile = await saveFileToS3({
      page,
      trigger: downloadLocator,
      timeoutInMs: 15000,
    })

    // Get signed URL for access
    const signedUrl = await uploadedFile.getSignedUrl()
    console.log(
      `Download uploaded file by clicking on: \x1b[1;4;36m\x1b]8;;${signedUrl}\x1b\\Click here\x1b]8;;\x1b\\\x1b[0m \n`
    )
    return uploadedFile
  }
  ```

  ```python Python theme={null}
  from intuned_browser import download_file
  from intuned_browser.helpers import save_file_to_s3
  from playwright.async_api import Page

  async def automation(page: Page, params, **_kwargs):
      await page.goto("https://sandbox.intuned.dev/pdfs")

      # Locate the download button
      download_locator = page.locator("xpath=//tbody/tr[1]//*[name()='svg']")

      # Download and upload to S3 in one step
      uploaded_file = await save_file_to_s3(
          page=page,
          trigger=download_locator,
          timeout_s=15,
      )

      # Get signed URL for access
      signed_url = await uploaded_file.get_signed_url()
      print(
          f"Download uploaded file by clicking on: \033[1;4;36m\033]8;;{signed_url}\033\\Click here\033]8;;\033\\\033[0m \n"
      )
      return uploaded_file
  ```
</CodeGroup>

## Related SDK methods

<CardGroup>
  <Card title="uploadFileToS3 (TypeScript)" icon="js" href="/automation-sdks/intuned-sdk/typescript/helpers/functions/uploadFileToS3">
    TypeScript SDK helper for uploading files to S3
  </Card>

  <Card title="upload_file_to_s3 (Python)" icon="python" href="/automation-sdks/intuned-sdk/python/helpers/functions/upload_file_to_s3">
    Python SDK helper for uploading files to S3
  </Card>

  <Card title="saveFileToS3 (TypeScript)" icon="js" href="/automation-sdks/intuned-sdk/typescript/helpers/functions/saveFileToS3">
    TypeScript SDK helper for downloading and uploading to S3 in one step
  </Card>

  <Card title="save_file_to_s3 (Python)" icon="python" href="/automation-sdks/intuned-sdk/python/helpers/functions/save_file_to_s3">
    Python SDK helper for downloading and uploading to S3 in one step
  </Card>

  <Card title="Cookbook (Python)" icon="github" href="https://github.com/Intuned/cookbook/tree/main/python-examples/quick-recipes">
    Python quick recipes in the Intuned Cookbook
  </Card>

  <Card title="Cookbook (TypeScript)" icon="github" href="https://github.com/Intuned/cookbook/tree/main/typescript-examples/quick-recipes">
    TypeScript quick recipes in the Intuned Cookbook
  </Card>
</CardGroup>


# AuthSessions
Source: https://intunedhq.com/docs/main/02-features/auth-sessions

Build authenticated browser automations and manage login sessions in Intuned

## Overview

Many browser automations need login—internal tools, apps without APIs, user-specific data, back-office workflows. AuthSessions handle this by saving browser state (cookies, local storage, session tokens) after a successful login. Intuned validates this state before each Run and recreates it automatically when sessions expire.

## Setup

### 1. Enable AuthSessions for your project

<Tabs>
  <Tab title="IDE">
    1. Open `Intuned.json` in your project.
    2. Set `authSessions.enabled` to `true` (or toggle **Enable AuthSessions** in the UI).
    3. Two files are created: `auth-sessions/create` and `auth-sessions/check`.

       <img alt="Enable AuthSessions" />

    Once enabled, all APIs in the project require an AuthSession to run.
  </Tab>

  <Tab title="CLI">
    **1. Enable AuthSessions in configuration**

    Open `Intuned.json` in your project and do the following changes to add the AuthSessions configuration:

    * Set `authSessions.enabled` to `true`
    * Set `authSessions.type` to `"API"`

    ```json theme={null}
    {
     // ... other configuration ...
      "authSessions": {
        "enabled": true,
        "type": "API"
      }
    }
    ```

    **2. Create the auth-sessions directory and files**

    <CodeGroup>
      ```bash TypeScript theme={null}
      mkdir -p auth-sessions
      touch auth-sessions/create.ts auth-sessions/check.ts
      ```

      ```bash Python theme={null}
      mkdir -p auth-sessions
      touch auth-sessions/create.py auth-sessions/check.py
      ```
    </CodeGroup>

    Once enabled, all APIs in the project require an AuthSession to run.
  </Tab>
</Tabs>

### 2. Implement the create API

The create API (`auth-sessions/create`) performs your login flow. It receives credentials and does whatever's needed to authenticate—navigate to login pages, fill forms, handle redirects. After it completes, Intuned captures the browser state.

<CodeGroup>
  ```typescript auth-sessions/create.ts theme={null}
  import { BrowserContext, Page } from "playwright";
  import { goToUrl } from "@intuned/browser";

  export interface CreateParams {
    username: string;
    password: string;
  }

  export default async function create(
    params: CreateParams,
    page: Page,
    context: BrowserContext
  ): AsyncGenerator<unknown, any, string> {
    await goToUrl({ page, url: "https://opensource-demo.orangehrmlive.com/" });
    await page.getByPlaceholder("Username").fill(params.username);
    await page.getByPlaceholder("Password").fill(params.password);
    await page.getByRole("button", { name: "Login" }).click();
    await page.waitForURL(
      "https://opensource-demo.orangehrmlive.com/web/index.php/dashboard/index"
    );
  }
  ```

  ```python auth-sessions/create.py theme={null}
  from playwright.async_api import Page
  from typing import TypedDict


  class Params(TypedDict):
      username: str
      password: str


  async def create(page: Page, params: Params | None = None, **_kwargs):
      await page.goto("https://opensource-demo.orangehrmlive.com/")
      await page.get_by_placeholder("Username").fill(params["username"])
      await page.get_by_placeholder("Password").fill(params["password"])
      await page.get_by_role("button", name="Login").click()
      await page.wait_for_url(
          "https://opensource-demo.orangehrmlive.com/web/index.php/dashboard/index"
      )
  ```
</CodeGroup>

### 3. Implement the check API

The check API (`auth-sessions/check`) validates that a session is still authenticated. It runs before every Run to ensure your automation never executes with invalid credentials.

<CodeGroup>
  ```typescript auth-sessions/check.ts theme={null}
  import { BrowserContext, Page } from "playwright";
  import { goToUrl } from "@intuned/browser";

  export default async function check(
    page: Page,
    context: BrowserContext
  ): Promise<boolean> {
    await goToUrl({
      page,
      url: "https://opensource-demo.orangehrmlive.com/web/index.php/dashboard/index",
    });

    const redirectedToLogin =
      page.url() ===
      "https://opensource-demo.orangehrmlive.com/web/index.php/auth/login";

    return !redirectedToLogin;
  }
  ```

  ```python auth-sessions/check.py theme={null}
  from playwright.async_api import Page


  async def check(page: Page, **_kwargs) -> bool:
      await page.goto(
          "https://opensource-demo.orangehrmlive.com/web/index.php/dashboard/index"
      )

      redirected_to_login = (
          page.url
          == "https://opensource-demo.orangehrmlive.com/web/index.php/auth/login"
      )

      return not redirected_to_login
  ```
</CodeGroup>

**Tips for the check API:**

* Target lightweight pages or specific elements—avoid heavy page loads
* Check for content that only appears when authenticated (user menu, account name)
* Don't just check for absence of login form—verify presence of authenticated content
* If the project handles more than one login flow on the same site (for example, different roles or user types that land on different post-login screens), read the parameters that were passed to `create` with [`getAuthSessionParameters`](/docs/main/05-references/runtime-sdk-typescript/get-auth-session-parameters) and branch the check accordingly

### 4. Create an AuthSession

Once you have deployed your project with the create and check APIs, you can create an AuthSession and use it in your Runs. Check the deployment documentation if you need help deploying your project.

The following examples show how to create an AuthSession via API, dashboard, or code.

<Tabs>
  <Tab title="API">
    <CodeGroup>
      ```typescript create-intuned-auth-session.ts theme={null}

        import { IntunedClient } from "@intuned/client";

        const intunedClient = new IntunedClient({
          workspaceId: "123e4567-e89b-12d3-a456-426614174000",
          apiKey: process.env["INTUNED_API_KEY"] ?? "",
        });

        async function run() {
          const result = await intunedClient.projects.authSessions.create.start("my-project", {
            parameters: {
              "username": "john.doe",
              "password": "password",
            },
            proxy: "http://proxy.example.com:8080",
          });

          console.log(result);
        }

        run();
      ```

      ```python create-intuned-auth-session.py theme={null}
      from intuned_client import IntunedClient
      from intuned_client import models
      import os


      with IntunedClient(
          workspace_id="123e4567-e89b-12d3-a456-426614174000",
          api_key=os.getenv("INTUNED_API_KEY", ""),
      ) as ic_client:

          res = ic_client.projects.auth_sessions.create.start(
              project_name="my-project",
              body=models.AuthSessionsCreateStartRequestBody(
                  parameters={
                      "username": "john.doe",
                      "password": "password",
                  },
                  id="auth-session-123",
                  proxy="http://proxy.example.com:8080",
                  create_attempts=3,
                  check_attempts=3,
                  save_trace=True,
              ),
          )

          # Handle response
          print(res)
      ```

      ```bash create-intuned-auth-session.sh theme={null}
      #!/bin/bash
      curl  --request POST \
            --url https://app.intuned.io/api/v1/workspace/{workspaceId}/projects/{projectName}/auth-sessions/create \
            --header 'Content-Type: application/json' \
            --header 'x-api-key: <api-key>' \
            --data '
          {
            "id": "auth-session-123",
            "parameters": {
              "username": "john.doe",
              "password": "password"
            },
            "proxy": "http://proxy.example.com:8080",
            "createAttempts": 3,
            "checkAttempts": 3,
            "saveTrace": true,
            "requestTimeout": 60000
          }
      '
      ```
    </CodeGroup>

    <Info>
      Check the [AuthSessions Create API Playground](/docs/client-apis/api-reference/projectsauthsessionscreate/create-authsession--start) for more details on creating AuthSessions via API.
    </Info>
  </Tab>

  <Tab title="Dashboard">
    1. Go to the **AuthSessions** tab in your project.
    2. Select **New AuthSession**.
    3. Enter credentials (and proxy if needed).
    4. Select **Save**.

       <img alt="Create Auth Session steps" />
  </Tab>

  <Tab title="CLI">
    <Note>
      Run from your project directory (where Intuned Settings is located), or specify the project explicitly with `-p <project-name>`.
    </Note>

    ```bash theme={null}
    intuned platform authsessions create <auth-session-id> \
      --input '{"username": "john.doe", "password": "password"}' \
      --wait
    ```

    Use `--proxy` to route traffic through a proxy:

    ```bash theme={null}
    intuned platform authsessions create my-auth-session \
      --input '{"username": "john.doe", "password": "password"}' \
      --proxy "http://proxy.example.com:8080" \
      --wait
    ```

    The `--wait` flag waits for the auth session creation to complete (defaults to 10 minutes). The `--input` accepts a JSON string or a path to a JSON file prefixed with `@`:

    ```bash theme={null}
    intuned platform authsessions create my-auth-session --input @credentials.json --wait
    ```

    See [Auth Sessions Create](/docs/main/05-references/cli/platform#auth-sessions-create) for all options.
  </Tab>

  <Tab title="Code">
    Define AuthSession instances in `intuned-resources/auth-sessions/` and deploy them with your project. This is separate from the `auth-sessions/` directory, which still contains your `create` and `check` automation logic.

    ```jsonc intuned-resources/auth-sessions/test-auth-session.auth-session.jsonc theme={null}
    {
      "parameters": {
        "username": "demo@email.com",
        "password": "DemoUser2024!"
      }
    }
    ```

    * `test-auth-session.auth-session.jsonc` creates an AuthSession with ID `test-auth-session`.
    * `.json` and `.jsonc` are both supported.
    * After deployment, the AuthSession appears in the **AuthSessions** tab with **Source** shown as `Created via code`.
    * New and updated code-defined AuthSessions automatically trigger a create run, so they may appear as pending until they become ready.
    * Update the file and redeploy to change the AuthSession.
    * Remove the file and redeploy to delete the AuthSession. If it is currently used by an active Run or JobRun, deletion is skipped until it is no longer in use.
    * Code-defined AuthSessions are read-only in the dashboard and API. Edit the file instead.
  </Tab>
</Tabs>

### 5. Use the AuthSession in your Runs

When starting a Run, specify the AuthSession to use. Here's an example of starting a Run with an existing AuthSession:

<CodeGroup>
  ```typescript trigger-intuned-run-with-auth-session.ts theme={null}
  import { IntunedClient } from "@intuned/client";

  const intunedClient = new IntunedClient({
    workspaceId: "123e4567-e89b-12d3-a456-426614174000",
    apiKey: process.env["INTUNED_API_KEY"] ?? "",
  });

  async function run() {
    const result = await intunedClient.projects.runs.start("my-project", {
      parameters: {
        "param1": "value1",
        "param2": 42,
        "param3": true,
      },
      api: "my-awesome-api",
      authSession: {
        id: "auth-session-123",
      },
    });

    console.log(result);
  }

  run();
  ```

  ```python trigger-intuned-run-with-auth-session.py theme={null}
  from intuned_client import IntunedClient
  from intuned_client import models
  import os


  with IntunedClient(
      workspace_id="123e4567-e89b-12d3-a456-426614174000",
      api_key=os.getenv("INTUNED_API_KEY", ""),
  ) as ic_client:

      res = ic_client.projects.runs.start(
        project_name="my-project", 
        body=models.RunStartRequestBody(
          parameters={
              "param1": "value1",
              "param2": 42,
              "param3": True,
          },
          api="my-awesome-api",
          auth_session={
              "id": "auth-session-123"
          },
        ),
      )

      # Handle response
      print(res)
  ```

  ```bash trigger-intuned-run-with-auth-session.sh theme={null}
  #!/bin/bash
  curl --request POST \
    --url https://app.intuned.io/api/v1/workspace/{workspaceId}/projects/{projectName}/run/start \
    --header 'Content-Type: application/json' \
    --header 'x-api-key: <api-key>' \
    --data '
    {
      "api": "my-awesome-api",
      "parameters": {
        "param1": "value1",
        "param2": 42,
        "param3": true
      },
      "saveTrace": true,
      "requestTimeout": 600,
      "authSession": {
        "id": "auth-session-123"
      }
    }
  '
  ```

  ```bash intuned-cli theme={null}
  intuned platform runs start '{
    "api": "my-awesome-api",
    "parameters": {
      "param1": "value1",
      "param2": 42,
      "param3": true
    },
    "authSession": {
      "id": "auth-session-123"
    }
  }'
  ```
</CodeGroup>

<Info>
  Check the [Runs API Playground](/docs/client-apis/api-reference/projectsruns/run-api--start) for more details on starting Runs via API.
</Info>

That's it—Intuned validates the session before running your API and recreates it automatically if it's expired.

## How AuthSessions work

When you run an API in an AuthSession-enabled project:

1. **Validation**: Before your code runs, Intuned checks if the AuthSession is still authenticated by running your `check` API.
2. **Auto-recreation**: If validation fails and auto-recreation is enabled (the default), Intuned runs your `create` API to log in again, then re-validates.
3. **Execution**: Only after validation succeeds does your API code run.

This happens for every Run attempt, so your automation never runs with invalid credentials.

### AuthSession Runs in your project

AuthSession operations appear as special Run types in your Runs list:

| Run type               | When it triggers                                    | What it does                                                                            |
| ---------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `AuthSession:Validate` | Before each API Run attempt                         | Runs `check` API; if it fails and auto-recreate is on, runs `create` then `check` again |
| `AuthSession:Create`   | When you create an AuthSession via dashboard or API | Runs `create` API, then `check` to confirm success                                      |
| `AuthSession:Update`   | When you update an AuthSession's credentials        | Runs `create` with new credentials, then `check` to confirm                             |

If an `AuthSession:Validate` fails, the dependent API Run is canceled (not marked as failed).

<Tip>
  For a detailed look at how AuthSessions integrate into Intuned's execution
  model, see [Intuned in depth →
  AuthSessions](/docs/main/01-learn/deep-dives/intuned-indepth#authsessions).
</Tip>

## Usage patterns

AuthSessions support three common patterns:

**Single account automation:** One account performs multiple operations. Create a single AuthSession and use it to start all Runs. Good for internal tools where you scrape analytics, export reports, and update settings—each as a separate API, all sharing one AuthSession.

**Multi-account automation:** Each user has their own AuthSession. Your app creates an AuthSession when a user "connects" their account, then runs automations on their behalf. Good for tools like social media managers where each user connects their own account.

**Multiple login flows on the same site:** A single site exposes more than one login flow (for example, a healthcare portal with separate provider and patient logins, or an admin login alongside an end-user login). The project's `create` and `check` APIs are shared, so include a parameter when you create the AuthSession that identifies which flow it represents, branch on it inside `create`, and read it back inside `check` with [`getAuthSessionParameters`](/docs/main/05-references/runtime-sdk-typescript/get-auth-session-parameters) to validate the correct post-login state.

## AuthSession types

The setup above uses **credentials-based** AuthSessions, which is the most common type. Intuned stores your credentials securely and uses them for automatic recreation when sessions expire.

Two other types are available for specific situations:

| Type                  | Who authenticates | Auto-recreation | When to use                              |
| --------------------- | ----------------- | --------------- | ---------------------------------------- |
| **Credentials-based** | Code              | ✓               | Standard login flows (default)           |
| **Runtime-based**     | Code              | ✓               | Changing credentials (OTPs, temp tokens) |
| **Recorder-based**    | Human             | ✗               | Complex login (MFA, CAPTCHA, biometrics) |

### Runtime-based AuthSessions

Pass credentials with each Run instead of storing them. Use this when credentials change frequently (like OTPs) or can't be stored.

<CodeGroup>
  ```typescript trigger-intuned-run-with-runtime-auth-session.ts theme={null}
  import { IntunedClient } from "@intuned/client";

  const intunedClient = new IntunedClient({
    workspaceId: "123e4567-e89b-12d3-a456-426614174000",
    apiKey: process.env["INTUNED_API_KEY"] ?? "",
  });

  async function run() {
    const result = await intunedClient.projects.runs.start("my-project", {
      parameters: {
        "param1": "value1",
        "param2": 42,
        "param3": true,
      },
      api: "my-awesome-api",
      authSession: {
        id: "auth-session-123", // Optional-omit to create new each time
        runtimeInput: {
          username: "user@example.com",
          otp: "123456",
        },
      },
    });

    console.log(result);
  }

  run();
  ```

  ```python trigger-intuned-run-with-runtime-auth-session.py theme={null}
  from intuned_client import IntunedClient
  from intuned_client import models
  import os


  with IntunedClient(
      workspace_id="123e4567-e89b-12d3-a456-426614174000",
      api_key=os.getenv("INTUNED_API_KEY", ""),
  ) as ic_client:

      res = ic_client.projects.runs.start(
        project_name="my-project", 
        body=models.RunStartRequestBody(
          parameters={
              "param1": "value1",
              "param2": 42,
              "param3": True,
          },
          api="my-awesome-api",
          auth_session={
              "id": "auth-session-123",  # Optional—omit to create new each time
              "runtime_input": {
                  "username": "user@example.com",
                  "otp": "123456",
              },
          },
        ),
      )

      # Handle response
      print(res)
  ```

  ```bash trigger-intuned-run-with-runtime-auth-session.sh theme={null}
  #!/bin/bash
  curl --request POST \
    --url https://app.intuned.io/api/v1/workspace/{workspaceId}/projects/{projectName}/run/start \
    --header 'Content-Type: application/json' \
    --header 'x-api-key: <api-key>' \
    --data '
    {
      "api": "my-awesome-api",
      "parameters": {
        "param1": "value1",
        "param2": 42,
        "param3": true
      },
      "saveTrace": true,
      "requestTimeout": 600,
      "authSession": {
        "id": "auth-session-123",
        "runtimeInput": {
          "username": "user@example.com",
          "otp": "123456"
        }
      }
    }
  '
  ```
</CodeGroup>

### Recorder-based AuthSessions

Recorder-based AuthSessions let a human log in through a hosted browser. Intuned captures the authenticated state after login completes. Use this for login flows that require human interaction (MFA apps, biometrics, complex CAPTCHAs).

<Note>
  To enable recorder-based AuthSessions, contact support via Slack or
  [email](mailto:support@intunedhq.com).
</Note>

## Configuration options

### Validation and recreation settings

Control how Intuned handles validation and recreation per Run.

<CodeGroup>
  ```typescript trigger-intuned-run-with-validation-settings.ts theme={null}
  import { IntunedClient } from "@intuned/client";

  const intunedClient = new IntunedClient({
    workspaceId: "123e4567-e89b-12d3-a456-426614174000",
    apiKey: process.env["INTUNED_API_KEY"] ?? "",
  });

  async function run() {
    const result = await intunedClient.projects.runs.start("my-project", {
      parameters: {
        "param1": "value1",
        "param2": 42,
        "param3": true,
      },
      api: "my-awesome-api",
      authSession: {
        id: "auth-session-123",
        autoRecreate: true,   // Recreate if validation fails (default: true)
        checkAttempts: 3,     // Retry validation this many times
        createAttempts: 3,    // Retry recreation this many times
      },
    });

    console.log(result);
  }

  run();
  ```

  ```python trigger-intuned-run-with-validation-settings.py theme={null}
  from intuned_client import IntunedClient
  from intuned_client import models
  import os


  with IntunedClient(
      workspace_id="123e4567-e89b-12d3-a456-426614174000",
      api_key=os.getenv("INTUNED_API_KEY", ""),
  ) as ic_client:

      res = ic_client.projects.runs.start(
        project_name="my-project", 
        body=models.RunStartRequestBody(
          parameters={
              "param1": "value1",
              "param2": 42,
              "param3": True,
          },
          api="my-awesome-api",
          auth_session={
              "id": "auth-session-123",
              "auto_recreate": True,   # Recreate if validation fails (default: true)
              "check_attempts": 3,     # Retry validation this many times
              "create_attempts": 3,    # Retry recreation this many times
          },
        ),
      )

      # Handle response
      print(res)
  ```

  ```bash trigger-intuned-run-with-validation-settings.sh theme={null}
  #!/bin/bash
  curl --request POST \
    --url https://app.intuned.io/api/v1/workspace/{workspaceId}/projects/{projectName}/run/start \
    --header 'Content-Type: application/json' \
    --header 'x-api-key: <api-key>' \
    --data '
    {
      "api": "my-awesome-api",
      "parameters": {
        "param1": "value1",
        "param2": 42,
        "param3": true
      },
      "saveTrace": true,
      "requestTimeout": 600,
      "authSession": {
        "id": "auth-session-123",
        "autoRecreate": true,
        "checkAttempts": 3,
        "createAttempts": 3
      }
    }
  '
  ```
</CodeGroup>

| Option           | Default | Description                                                         |
| ---------------- | ------- | ------------------------------------------------------------------- |
| `autoRecreate`   | `true`  | If validation fails, run `create` to get a new session              |
| `checkAttempts`  | 1       | Times to retry the `check` API before considering validation failed |
| `createAttempts` | 1       | Times to retry the `create` API if recreation triggers              |

### Updating credentials

Update stored credentials for a credentials-based AuthSession:

<Tabs>
  <Tab title="API / SDK">
    <CodeGroup>
      ```typescript trigger-intuned-auth-session-update.ts theme={null}
      import { IntunedClient } from "@intuned/client";

      const intunedClient = new IntunedClient({
        workspaceId: "123e4567-e89b-12d3-a456-426614174000",
        apiKey: process.env["INTUNED_API_KEY"] ?? "",
      });

      async function run() {
        const result = await intunedClient.projects.authSessions.update.start("my-project", "<id>", {
          parameters: {
            "username": "john.doe",
            "password": "newPassword",
          },
          proxy: "http://proxy.example.com:8080",
        });

        console.log(result);
      }

      run();
      ```

      ```python trigger-intuned-auth-session-update.py theme={null}
      from intuned_client import IntunedClient
      from intuned_client import models
      import os


      with IntunedClient(
          workspace_id="123e4567-e89b-12d3-a456-426614174000",
          api_key=os.getenv("INTUNED_API_KEY", ""),
      ) as ic_client:
          res = ic_client.projects.auth_sessions.update.start(
            project_name="my-project", 
            auth_session_id="<id>", 
            body=models.AuthSessionsUpdateStartRequestBody(
              parameters={
                "username": "john.doe",
                "password": "newPassword",
              },
              proxy="http://proxy.example.com:8080",
            ),
          )

          # Handle response
          print(res)
      ```

      ```bash trigger-intuned-auth-session-update.sh theme={null}
      #!/bin/bash
      curl --request POST \
        --url https://app.intuned.io/api/v1/workspace/{workspaceId}/projects/{projectName}/auth-sessions/{authSessionId}/update \
        --header 'Content-Type: application/json' \
        --header 'x-api-key: <api-key>' \
        --data '
      {
        "parameters": {
          "username": "john.doe",
          "password": "newPassword"
        },
        "proxy": "http://proxy.example.com:8080",
        "createAttempts": 3,
        "checkAttempts": 3,
        "saveTrace": true
      }
      '
      ```
    </CodeGroup>
  </Tab>

  <Tab title="CLI">
    <Note>
      Run from your project directory (where Intuned Settings is located), or specify the project explicitly with `-p <project-name>`.
    </Note>

    ```bash theme={null}
    intuned platform authsessions update <auth-session-id> \
      --input '{"username": "john.doe", "password": "newPassword"}' \
      --wait
    ```

    With a proxy:

    ```bash theme={null}
    intuned platform authsessions update my-auth-session \
      --input '{"username": "john.doe", "password": "newPassword"}' \
      --proxy "http://proxy.example.com:8080" \
      --wait
    ```

    See [Auth Sessions Update](/docs/main/05-references/cli/platform#auth-sessions-update) for all options.
  </Tab>
</Tabs>

### Proxy support

Configure a proxy when creating an AuthSession. All Runs using that AuthSession—including validation and recreation—route through the proxy.

<CodeGroup>
  ```typescript create-intuned-auth-session-with-proxy.ts theme={null}
  import { IntunedClient } from "@intuned/client";

  const intunedClient = new IntunedClient({
    workspaceId: "123e4567-e89b-12d3-a456-426614174000",
    apiKey: process.env["INTUNED_API_KEY"] ?? "",
  });

  async function run() {
    const result = await intunedClient.projects.authSessions.create.start("my-project", {
      parameters: {
        "username": "john.doe",
        "password": "password",
      },
      proxy: "http://proxy.example.com:8080",
    });

    console.log(result);
  }

  run();
  ```

  ```python create-intuned-auth-session-with-proxy.py theme={null}
  from intuned_client import IntunedClient
  from intuned_client import models
  import os


  with IntunedClient(
      workspace_id="123e4567-e89b-12d3-a456-426614174000",
      api_key=os.getenv("INTUNED_API_KEY", ""),
  ) as ic_client:

      res = ic_client.projects.auth_sessions.create.start(
        project_name="my-project", 
        body=models.AuthSessionsCreateStartRequestBody(
          parameters={
              "username": "john.doe",
              "password": "password",
          },
          id="auth-session-123",
          proxy="http://proxy.example.com:8080",
        ),
      )

      # Handle response
      print(res)
  ```

  ```bash create-intuned-auth-session-with-proxy.sh theme={null}
  #!/bin/bash
  curl --request POST \
    --url https://app.intuned.io/api/v1/workspace/{workspaceId}/projects/{projectName}/auth-sessions/create \
    --header 'Content-Type: application/json' \
    --header 'x-api-key: <api-key>' \
    --data '
    {
      "id": "auth-session-123",
      "parameters": {
        "username": "john.doe",
        "password": "password"
      },
      "proxy": "http://proxy.example.com:8080"
    }
  '
  ```
</CodeGroup>

## Using AuthSessions in Jobs

Specify the AuthSession in the Job configuration. All Runs in the JobRun use the same AuthSession.

<CodeGroup>
  ```typescript create-intuned-job-with-auth-session.ts theme={null}
  import { IntunedClient } from "@intuned/client";

  const intunedClient = new IntunedClient({
    workspaceId: "123e4567-e89b-12d3-a456-426614174000",
    apiKey: process.env["INTUNED_API_KEY"] ?? "",
  });

  async function run() {
    const result = await intunedClient.projects.jobs.create("my-project", {
      id: "my-awesome-job",
      payload: [
        {
          parameters: {
            "param1": "value1",
            "param2": 42,
            "param3": true,
          },
          apiName: "my-awesome-api",
        },
      ],
      configuration: {
        retry: {
          "maximumAttempts": 3,
        },
      },
      auth_session: {
        id: "auth-session-123",
      },
    });

    console.log(result);
  }

  run();
  ```

  ```python create-intuned-job-with-auth-session.py theme={null}
  from intuned_client import IntunedClient
  from intuned_client import models
  import os


  with IntunedClient(
      workspace_id="123e4567-e89b-12d3-a456-426614174000",
      api_key=os.getenv("INTUNED_API_KEY", ""),
  ) as ic_client:

      res = ic_client.projects.jobs.create(
          project_name="my-project",
          body=models.JobsCreateRequestBody(
              id="my-awesome-job",
              payload=[
                  {
                      "parameters": {
                          "param1": "value1",
                          "param2": 42,
                          "param3": True,
                      },
                      "api_name": "my-awesome-api",
                  },
              ],
              configuration={
                  "retry": {
                      "maximum_attempts": 3,
                  },
              },
              auth_session={
                  "id": "<id>",
              },
          ),
      )

      # Handle response
      print(res)
  ```

  ```bash create-intuned-job-with-auth-session.sh theme={null}
  #!/bin/bash
  curl --request POST \
    --url https://app.intuned.io/api/v1/workspace/{workspaceId}/projects/{projectName}/jobs \
    --header 'Content-Type: application/json' \
    --header 'x-api-key: <api-key>' \
    --data '
  {
    "id": "my-awesome-job",
    "configuration": {
      "retry": {
        "maximumAttempts": 3
      }
    },
    "payload": [
      {
        "apiName": "my-awesome-api",
        "parameters": {
          "param1": "value1",
          "param2": 42,
          "param3": true
        }
      }
    ],
    "auth_session": {
      "id": "auth-session-123"
    }
  }
  '
  ```
</CodeGroup>

Jobs validate the AuthSession when the JobRun starts and before each Run attempt. If validation fails during execution and can't recover, the JobRun pauses—you can fix the AuthSession manually and resume it from where it paused.

## Best practices

* **Make check fast and reliable.** Target lightweight endpoints or specific DOM elements. Check for authenticated content, not just absence of login forms.
* **Use single-account pattern for internal tools.** Fewer AuthSessions to manage and simpler credential rotation.
* **Separate AuthSessions per user.** For multi-user integrations, never share AuthSessions across users.
* **Set appropriate retry attempts.** Increase `checkAttempts` for flaky networks; increase `createAttempts` if login sometimes fails transiently.
* **Enable CAPTCHA solving if needed.** Configure in `Intuned.json` if your target system uses CAPTCHAs during login.

## Limitations

* AuthSession type can't change after creation—create a new one to switch types.
* Recorder-based AuthSessions can't auto-recreate; they need human intervention when expired.
* Validation adds latency since check runs before every attempt—keep your check API fast.
* AuthSessions are project-scoped; you can't use one from another project.

## FAQs

<AccordionGroup>
  <Accordion title="How long do AuthSessions last?">
    AuthSessions persist indefinitely in Intuned, but the underlying session with the target system depends on that system's policies. Some expire after hours, others after weeks. With auto-recreation enabled, AuthSessions effectively last as long as your credentials remain valid.
  </Accordion>

  <Accordion title="What happens if my password changes?">
    For credentials-based AuthSessions, use the update API to provide new
    credentials. For runtime-based, pass the new credentials in your next Run. For
    recorder-based, have a human log in again through the recorder.
  </Accordion>

  <Accordion title="Validation keeps failing even though I can log in manually">
    Your check API may not accurately detect authenticated state. Common issues:
    checking before the page fully loads, checking for elements that appear on
    both authenticated and unauthenticated pages, or not handling redirects
    properly. Review your check API to ensure it waits for page load and checks
    for content that *only* appears when authenticated.
  </Accordion>

  <Accordion title="Auto-recreation triggers too often">
    Frequent recreation may indicate incorrect check logic or sessions expiring faster than expected. Verify your check API isn't too strict (failing on transient issues), and investigate the target system's session duration. Consider increasing `checkAttempts` to handle transient failures.
  </Accordion>

  <Accordion title="Can one project handle multiple login flows on the same site?">
    Yes. A project has one `create` and one `check` API, so when a site exposes more than one login flow (for example, separate provider and patient logins on a healthcare portal), pass a parameter when you create each AuthSession that identifies the flow, branch on it inside `create`, and read it back inside `check` with [`getAuthSessionParameters`](/docs/main/05-references/runtime-sdk-typescript/get-auth-session-parameters) so each AuthSession is validated against the correct post-login state. See the [multiple login flows on the same site](#usage-patterns) usage pattern.
  </Accordion>
</AccordionGroup>

## Related resources

<CardGroup>
  <Card title="AuthSessions API reference" icon="code" href="/client-apis/api-reference/projectsauthsessions/get-authsessions">
    Complete API documentation for AuthSession operations
  </Card>

  <Card title="Intuned in depth" icon="book" href="/main/01-learn/deep-dives/intuned-indepth#authsessions">
    Detailed explanation of how AuthSessions integrate with Intuned's execution
    model
  </Card>

  <Card title="Jobs and batched executions" icon="list-check" href="/main/02-features/jobs-batched-executions">
    Use AuthSessions with scheduled and batched workflows
  </Card>

  <Card title="Stealth mode, CAPTCHA, and proxies" icon="shield" href="/main/02-features/stealth-mode-captcha-solving-proxies">
    Configure CAPTCHA solving and proxies for authentication
  </Card>
</CardGroup>


# CLI
Source: https://intunedhq.com/docs/main/02-features/cli



The Intuned CLI (`@intuned/cli`) is a command-line tool for developing, testing, deploying, and managing Intuned automation projects. It supports TypeScript and Python, and lets you manage deployed Projects, Runs, Jobs, and Auth Sessions from the terminal.

## Installation

Install the Intuned CLI globally with npm:

```bash theme={null}
npm install -g @intuned/cli
```

Verify the installation:

```bash theme={null}
intuned -v
```

<Note>Requires Node.js and npm.</Note>

## Authentication

The CLI supports two authentication methods:

1. **Browser-based login** — Run `intuned auth login` to authenticate via browser-based OAuth. Credentials are stored locally and reused across sessions.

2. **API key** — Set the `INTUNED_API_KEY` environment variable. Requires a workspace ID from the `INTUNED_WORKSPACE_ID` environment variable or `workspaceId` in your settings file.

If both are set, the API key takes priority.

<Tip>Use API key authentication for CI/CD pipelines and automated workflows.</Tip>

For full details on authentication methods and credentials storage, see [CLI configuration reference](/docs/main/05-references/cli/configuration).

## Configuration

### Settings file

The CLI reads project settings from a file in the root of your project directory. Supported formats (checked in order):

* `Intuned.json`
* `Intuned.jsonc`

The settings file contains configurations used for development and deployment. For the full schema, see [Intuned.json reference](/docs/main/05-references/intuned-json).

### Environment variables

Key environment variables for CLI configuration:

| Variable                | Description                                                                               |
| ----------------------- | ----------------------------------------------------------------------------------------- |
| `INTUNED_API_KEY`       | API key for authentication. Takes priority over browser-based login.                      |
| `INTUNED_WORKSPACE_ID`  | Workspace ID. Required when using API key authentication if not set in the settings file. |
| `INTUNED_CHROMIUM_PATH` | Path to a custom Chromium executable for the dev browser.                                 |

For the full list of environment variables, see [CLI configuration reference](/docs/main/05-references/cli/configuration).

<Info>These are CLI configuration variables. For project-level environment variables and secrets used in your automations, see [Environment variables and secrets](/docs/main/02-features/environment-variables-secrets).</Info>

## Key capabilities

### Local development

The `intuned dev` command group provides tools for developing and testing automations locally—run APIs, manage auth sessions, generate traces for debugging, and deploy your project when it's ready.

See [Local development](/docs/main/02-features/local-development-cli) for a complete walkthrough.

<Tip>Make sure you install your project's dependencies before running development commands.</Tip>

### Platform management

The `intuned platform` command group lets you manage projects on the Intuned platform. You can view and start Runs and Jobs, manage Auth Sessions and Environment Variables, and more.

See [Platform reference](/docs/main/05-references/cli/platform) for available commands.

### Project scaffolding

Create a new Intuned project with a template:

```bash theme={null}
intuned dev init
```

This walks you through selecting a language, choosing a template, naming your project, and connecting to your workspace. See [Local development](/docs/main/02-features/local-development-cli#create-a-project) for details.

## Related resources

<CardGroup>
  <Card title="Local development" icon="code" href="/main/02-features/local-development-cli">
    Complete guide to local development workflow
  </Card>

  <Card title="CLI reference" icon="terminal" href="/main/05-references/cli/overview">
    Full command reference and usage details
  </Card>

  <Card title="CLI configuration" icon="gear" href="/main/05-references/cli/configuration">
    Settings files, environment variables, and authentication
  </Card>

  <Card title="Quickstart" icon="rocket" href="/main/00-getting-started/quickstarts/scraper">
    Build your first automation
  </Card>

  <Card title="Intuned.json reference" icon="file-code" href="/main/05-references/intuned-json">
    Full settings file schema and options
  </Card>
</CardGroup>


# Environment variables and secrets
Source: https://intunedhq.com/docs/main/02-features/environment-variables-secrets



## Overview

Environment variables let you pass sensitive information—API keys, passwords, authentication tokens—to your Projects without exposing them in your code. Use them to authenticate with external services, configure different settings for development versus production, or share configuration across multiple Projects.

## How it works

Environment variables are defined at either the Project level or workspace level.

**Variable hierarchy**: Project-level variables take precedence over workspace-level variables. If you define the same key at both levels, the Project-level value is used. This lets you set workspace-wide defaults while allowing individual Projects to override specific values.

**Environment targeting**: Each variable can target three contexts: Online IDE (development), deployed Projects (production), or both. Use different API keys or configuration for development versus production.

## Usage

### Project-level environment variables

Project environment variables are scoped to a single project. Manage them via the dashboard at `https://app.intuned.io/projects/{project-id}/env-vars` or with the CLI from your project directory.

### Create an environment variable

<Tabs>
  <Tab title="Dashboard">
    Select **New** and provide a key, value, and optional description. Choose where to expose it: Online IDE, deployed Projects, or both.

    <Note>
      Deployed environment variables require redeployment to take effect. When developing locally via CLI, see [Local development](#local-development).
    </Note>

    <img alt="Create Environment Variable" />

    To use different values per environment, create two variables with the same key and select the appropriate environment for each.

    <img alt="Create Environment Variable Different Values" />
  </Tab>

  <Tab title="CLI">
    <Note>
      Run from your project directory (where Intuned Settings is located), or specify the project explicitly with `-p <project-name>`.
    </Note>

    ```bash theme={null}
    intuned platform env-vars create --key MY_VAR --value my-value
    ```

    Use `--envs` to target specific environments (comma-separated). Supported values: `PUBLISHED`, `AUTHORING`. Defaults to `PUBLISHED`.

    ```bash theme={null}
    # Create for both environments
    intuned platform env-vars create --key MY_VAR --value my-value --envs PUBLISHED,AUTHORING

    # With a description
    intuned platform env-vars create --key MY_VAR --value my-value --description "My API key"

    # Specify project explicitly
    intuned platform env-vars create -p my-project --key MY_VAR --value my-value
    ```

    To use different values per environment, create two variables with the same key using `--envs` to target each environment separately.

    <Note>
      Deployed environment variables require redeployment to take effect. When developing locally via CLI, see [Local development](#local-development).
    </Note>

    See [Env Vars Create](/docs/main/05-references/cli/platform#env-vars-create) for all options.
  </Tab>
</Tabs>

### Edit an environment variable

<Tabs>
  <Tab title="Dashboard">
    Select **...** > **Edit** next to a variable to update its key, value, or environments.

    <img alt="Edit Environment Variable" />
  </Tab>

  <Tab title="CLI">
    <Note>
      Run from your project directory (where Intuned Settings is located), or specify the project explicitly with `-p <project-name>`.
    </Note>

    ```bash theme={null}
    intuned platform env-vars update MY_VAR --value new-value
    ```

    See [Env Vars Update](/docs/main/05-references/cli/platform#env-vars-update) for all options.
  </Tab>
</Tabs>

### Delete an environment variable

<Tabs>
  <Tab title="Dashboard">
    Select **...** next to the variable you want to delete, then select **Delete**. Confirm when prompted.

    <img alt="Delete Environment Variable" />
  </Tab>

  <Tab title="CLI">
    <Note>
      Run from your project directory (where Intuned Settings is located), or specify the project explicitly with `-p <project-name>`.
    </Note>

    ```bash theme={null}
    intuned platform env-vars delete MY_VAR
    ```

    Add `--force` to skip the confirmation prompt:

    ```bash theme={null}
    intuned platform env-vars delete MY_VAR --force
    ```

    See [Env Vars Delete](/docs/main/05-references/cli/platform#env-vars-delete) for all options.
  </Tab>
</Tabs>

### Workspace-level environment variables

Workspace environment variables are defined at the workspace level and exposed to all Projects in that workspace.

<Tabs>
  <Tab title="Dashboard">
    Go to `https://app.intuned.io/settings/env-vars` to manage workspace-level variables.

    Access them from within a project by selecting **Shared variables** in the Environment Variables tab.

    <img alt="Workspace Level Environment Variables" />

    Alternatively, go to **side panel** > **Workspace Settings** > **Environment Variables**.

    <img alt="Workspace Level Environment Variables Side Panel" />
  </Tab>

  <Tab title="CLI">
    Manage workspace-level variables with the `workspace env-vars` subcommands:

    ```bash theme={null}
    # List workspace env vars
    intuned platform workspace env-vars list

    # Create a workspace env var
    intuned platform workspace env-vars create --key SHARED_KEY --value shared-value

    # Update a workspace env var
    intuned platform workspace env-vars update SHARED_KEY --value new-value

    # Delete a workspace env var
    intuned platform workspace env-vars delete SHARED_KEY
    ```

    See [Workspace Env Vars](/docs/main/05-references/cli/platform#workspace-env-vars-list) for all options.
  </Tab>
</Tabs>

### Local development

When developing locally via CLI, the CLI reads `.env` files and system environment variables.

```env .env theme={null}
TEST_VAR=local_value
```

<Note>
  `.env` files are for local development only and aren't deployed. Before
  deploying, configure your environment variables using the dashboard or CLI
  commands above.
</Note>

## Related resources

<CardGroup>
  <Card title="Online IDE" icon="code" href="/main/02-features/online-ide">
    Develop on Intuned with zero config
  </Card>

  <Card title="CLI" icon="terminal" href="/main/02-features/local-development-cli">
    Develop locally with your favorite IDE and tools
  </Card>
</CardGroup>


# Flexible automations
Source: https://intunedhq.com/docs/main/02-features/flexible-automation

Choose the right approach for your automation

## Overview

Intuned Projects are code-based. You write browser automations in Python or TypeScript, deploy them to Intuned's infrastructure, and run them on demand or on a schedule.

Since you're writing code, you decide the approach: deterministic scripts, AI-driven automation, crawlers, or a combination. This page covers the common patterns and when each one fits.

## Deterministic automations

Deterministic automations use Playwright (or other libraries) to interact with the browser directly. You write selectors, handle navigation, and extract data with explicit logic.

**Trade-offs:**

* Faster execution and lower cost per run (no AI inference)
* Predictable—same input, same output
* Requires upfront work to write and maintain selectors
* Breaks when site structure changes

**Common use cases:**

* Extracting structured data from a known site repeatedly (product prices, job listings, inventory)
* Monitoring sources where speed matters
* Form submissions and data entry where accuracy is critical

**Examples:**

* Product scraper — [Python](https://github.com/Intuned/cookbook/tree/main/python-examples/e-commerce-scrapingcourse) | [TypeScript](https://github.com/Intuned/cookbook/tree/main/typescript-examples/e-commerce-scrapingcourse)
* Form submission automation — [Python](https://github.com/Intuned/cookbook/tree/main/python-examples/rpa-example) | [TypeScript](https://github.com/Intuned/cookbook/tree/main/typescript-examples/rpa-example)

## AI-driven automations

AI-driven automations use LLMs to interpret pages and decide actions. You describe what you want instead of writing selectors.

Several libraries/APIs support this approach:

* **Stagehand** — AI-powered browser automation with natural language commands
* **Browser-Use** — Agent framework for browser tasks
* **Computer Use APIs** — Anthropic, OpenAI, Gemini, or any other computer use API
* **@Intuned/Browser** — Intuned's Browser SDK has many data extraction utilities

**Trade-offs:**

* Works across different sites without site-specific code
* Adapts to layout changes and variations
* Slower and more expensive per run
* Less predictable—output can vary

**Common use cases:**

* One-off scraping where selectors aren't worth maintaining
* Gathering data across many different sites (company research, market data)
* Automating workflows on sites that change frequently

**Examples:**

* Computer Use APIs — [Python](https://github.com/Intuned/cookbook/tree/main/python-examples/computer-use) | [TypeScript](https://github.com/Intuned/cookbook/tree/main/typescript-examples/computer-use)
* Browser-Use agent — [Python](https://github.com/Intuned/cookbook/tree/main/python-examples/browser-use)
* Stagehand automation — [Python](https://github.com/Intuned/cookbook/tree/main/python-examples/stagehand) | [TypeScript](https://github.com/Intuned/cookbook/tree/main/typescript-examples/stagehand)

## Crawlers

A crawler discovers and visits pages beyond a single URL. It follows links, handles pagination, or iterates through sitemaps to process content across a site.

**Crawl4AI** is an excellent library for building crawlers.

**Trade-offs:**

* Discovers pages automatically—you don't need every URL upfront
* Can process hundreds or thousands of pages per Job
* More complex to debug when something fails mid-crawl

**Common use cases:**

* Indexing documentation sites
* Collecting assets (PDFs, images) across a site
* Finding public contact information across directories
* Tracking content changes across competitor sites

**Examples:**

* Crawl4AI — [Python](https://github.com/Intuned/cookbook/tree/main/python-examples/crawl4ai)
* E-commerce nested crawler — [Python](https://github.com/Intuned/cookbook/tree/main/python-examples/e-commerce-nested) | [TypeScript](https://github.com/Intuned/cookbook/tree/main/typescript-examples/e-commerce-nested)
* Native crawler — [Python](https://github.com/Intuned/cookbook/tree/main/python-examples/native-crawler) | [TypeScript](https://github.com/Intuned/cookbook/tree/main/typescript-examples/native-crawler)

## Hybrid automations

You can combine approaches in a single Project. A few patterns that work well:

### Crawler with conditional parsing

A crawler visits pages across multiple sites. For known structures—job boards on Lever or Greenhouse—it uses deterministic parsers. For unknown sites, it falls back to AI.

This handles common cases quickly and affordably while covering edge cases.

View example (`hybrid-crawler/crawl` API) — [Python](https://github.com/Intuned/cookbook/tree/main/python-examples/hybrid-automation) | [TypeScript](https://github.com/Intuned/cookbook/tree/main/typescript-examples/hybrid-automation)

### Deterministic automation with AI fallback

A form automation runs deterministic code for the standard flow. If an element isn't found or the structure has changed, it catches the error and hands off to AI.

View example (`hybrid-rpa/fill-form` API) — [Python](https://github.com/Intuned/cookbook/tree/main/python-examples/hybrid-automation) | [TypeScript](https://github.com/Intuned/cookbook/tree/main/typescript-examples/hybrid-automation)

### Deterministic scraper with AI extraction

A scraper uses selectors for most fields. For fields that need interpretation—like categorizing unstructured text—it calls AI for just those extractions.

View example (`hybrid-scraper/list` & `hybrid-scraper/details` APIs) — [Python](https://github.com/Intuned/cookbook/tree/main/python-examples/hybrid-automation) | [TypeScript](https://github.com/Intuned/cookbook/tree/main/typescript-examples/hybrid-automation)

## Related resources

<CardGroup>
  <Card title="@Intuned/Browser SDK" icon="code" href="/automation-sdks/intuned-browser">
    Has some AI-powered data extraction helpers
  </Card>

  <Card title="Browser-Use integration" icon="robot" href="/main/04-integrations/browser-use">
    Run Browser-Use agents on Intuned infrastructure
  </Card>

  <Card title="Stagehand integration" icon="wand-magic-sparkles" href="/main/04-integrations/stagehand">
    Use Stagehand for AI-powered browser automation
  </Card>

  <Card title="Cookbook" icon="book" href="https://github.com/intuned/cookbook">
    Ready-to-use automation examples and recipes
  </Card>
</CardGroup>


# Jobs (batched executions)
Source: https://intunedhq.com/docs/main/02-features/jobs-batched-executions

Execute browser automations at scale with scheduled, batched, and dynamically expandable workflows

## Overview

Jobs are blueprints that define when, how, and what browser automations to execute. Instead of triggering individual API runs manually, Jobs let you orchestrate multiple automation executions together—running them in bulk, on a schedule, or both. They handle scheduling, retries, concurrency management, and result aggregation.

The most common use case: set up a scraper to run on a schedule and configure a sink to deliver results directly to your system. You create the scraper, configure the Job with a schedule and a webhook or S3 sink, and the data flows into your service automatically—no polling or manual exports required.

```
Job (blueprint)
  └── JobRun (execution instance)
        ├── Run 1 (API execution for payload item 1)
        │     └── Attempt 1, Attempt 2, etc.
        ├── Run 2 (API execution for payload item 2)
        │     └── Attempt 1, Attempt 2, etc.
        └── Run N (API execution for payload item N)
              └── Attempt 1, Attempt 2, etc.
```

## Usage

### Create a Job

<Tabs>
  <Tab title="Dashboard">
    <Steps>
      <Step title="Navigate to Jobs">
        Open your Project in the Intuned dashboard and select the **Jobs** tab.
      </Step>

      <Step title="Create new Job">
        Select **Create Job** to open the configuration editor. A UI wizard will guide you through defining the Job's ID, payload, schedule, concurrency, retries, and sink.

        <Frame>
          <img alt="Create job" />
        </Frame>
      </Step>
    </Steps>
  </Tab>

  <Tab title="API">
    See the [Create Job API endpoint](/docs/client-apis/api-reference/projectsjobs/create-job) for programmatic Job creation.
  </Tab>

  <Tab title="CLI">
    <Note>
      Run from your project directory (where Intuned Settings is located), or specify the project explicitly with `-p <project-name>`.
    </Note>

    Pass the Job configuration as a JSON string or a path to a JSON file:

    ```bash theme={null}
    intuned platform jobs create '{
      "id": "daily-sync",
      "payload": [{"apiName": "list", "parameters": {}}],
      "configuration": {"retry": {"maximumAttempts": 3}}
    }'
    ```

    With an AuthSession:

    ```bash theme={null}
    intuned platform jobs create '{
      "id": "daily-sync",
      "payload": [{"apiName": "list", "parameters": {}}],
      "configuration": {"retry": {"maximumAttempts": 3}},
      "auth_session": {
        "id": "auth-session-123"
      }
    }'
    ```

    With a webhook sink:

    ```bash theme={null}
    intuned platform jobs create '{
      "id": "daily-sync",
      "payload": [{"apiName": "list", "parameters": {}}],
      "configuration": {"retry": {"maximumAttempts": 3}},
      "sink": {
        "type": "webhook",
        "url": "<webhook-url>"
      }
    }'
    ```

    With an S3 sink:

    ```bash theme={null}
    intuned platform jobs create '{
      "id": "daily-sync",
      "payload": [{"apiName": "list", "parameters": {}}],
      "configuration": {"retry": {"maximumAttempts": 3}},
      "sink": {
        "type": "s3",
        "bucket": "<bucket-name>",
        "accessKeyId": "<access-key-id>",
        "secretAccessKey": "<secret-access-key>",
        "region": "<region>"
      }
    }'
    ```

    Using a file (recommended for complex configurations):

    ```bash theme={null}
    intuned platform jobs create @job-config.json
    ```

    See [Jobs Create](/docs/main/05-references/cli/platform#jobs-create) for all options.
  </Tab>

  <Tab title="Code">
    Define Jobs in `intuned-resources/jobs/` and deploy them with your project. Each file becomes one Job, and the filename becomes the Job ID.

    ```jsonc intuned-resources/jobs/daily-sync.job.jsonc theme={null}
    {
      "configuration": {
        "maxConcurrentRequests": 2,
        "retry": {
          "maximumAttempts": 3
        }
      },
      "payload": [
        {
          "apiName": "list",
          "parameters": {}
        }
      ],
      "schedule": {
        "calendars": [
          {
            "hour": 9,
            "minute": 0
          }
        ]
      }
    }
    ```

    * `daily-sync.job.jsonc` creates a Job with ID `daily-sync`.
    * `.json` and `.jsonc` are both supported.
    * After deployment, the Job appears in the **Jobs** tab with **Source** shown as `Created via code`.
    * Update the file and redeploy to change the Job. Remove the file and redeploy to delete it.
    * Code-defined Jobs are read-only in the dashboard and API. Edit the file instead.
  </Tab>
</Tabs>

### Trigger a JobRun

A Job can have a schedule, but the schedule is optional. Even if a Job has a schedule configured, you can trigger it on-demand whenever you want. Each trigger creates a new JobRun.

<Tabs>
  <Tab title="Dashboard">
    <Steps>
      <Step title="Navigate to Jobs">
        Go to the **Jobs** tab in your Project.
      </Step>

      <Step title="Find your Job">Locate your Job in the list.</Step>

      <Step title="Trigger the Job">
        Select **...** next to the Job, then select **Trigger**.
      </Step>
    </Steps>
  </Tab>

  <Tab title="API">
    See the [Trigger Job API
    endpoint](/docs/client-apis/api-reference/projectsjobs/trigger-job) for
    programmatic Job triggering.
  </Tab>

  <Tab title="CLI">
    <Note>
      Run from your project directory (where Intuned Settings is located), or specify the project explicitly with `-p <project-name>`.
    </Note>

    ```bash theme={null}
    intuned platform jobs trigger <job-id>
    ```

    See [Jobs Trigger](/docs/main/05-references/cli/platform#jobs-trigger) for all options.
  </Tab>
</Tabs>

### Monitor a Job

Intuned provides full observability into every JobRun, giving you complete visibility into what happened and why. Each execution generates detailed logs, browser traces, and session recordings that help you debug issues and understand your automation's behavior.

<Tabs>
  <Tab title="Dashboard">
    <Steps>
      <Step title="Open the Job you want to monitor">
        Navigate to the **Jobs** tab and select the job to view all the JobRuns
        for it.
      </Step>

      <Step title="Track progress">
        click on the job run to see overall status, progress, and summary of
        runs.
      </Step>

      <Step title="View Runs and Attempts">
        View individual Runs and their Attempts, including browser traces and
        logs.
      </Step>
    </Steps>
  </Tab>

  <Tab title="API">
    See the [Jobs API
    reference](/docs/client-apis/api-reference/projectsjobsruns/get-job-runs) for
    programmatic Job monitoring.
  </Tab>

  <Tab title="CLI">
    <Note>
      Run from your project directory (where Intuned Settings is located), or specify the project explicitly with `-p <project-name>`.
    </Note>

    List all JobRuns for a Job:

    ```bash theme={null}
    intuned platform jobruns list <job-id>
    ```

    Get details of a specific JobRun:

    ```bash theme={null}
    intuned platform jobruns get <job-run-id>
    ```

    Use `--json` to get machine-readable output:

    ```bash theme={null}
    intuned platform jobruns list <job-id> --json
    ```

    See [Job Runs List](/docs/main/05-references/cli/platform#jobruns-list) and [Job Runs Get](/docs/main/05-references/cli/platform#jobruns-get) for all options.
  </Tab>
</Tabs>

### Pause and resume a Job

Pausing a Job stops new JobRuns from starting and prevents in-progress JobRuns from executing new payload items. Currently running Runs will be canceled and retried when you resume. Use pause when you need to temporarily stop execution—for example, to fix an issue or update credentials.

<Tabs>
  <Tab title="Dashboard">
    **Pause:** Navigate to the **Jobs** tab, select **...** next to the Job, then select **Pause**.

    **Resume:** Select **...** next to the paused Job, then select **Resume**. This re-enables scheduling and continues any paused JobRuns from where they left off.
  </Tab>

  <Tab title="API">
    See the [Jobs API reference](/docs/client-apis/api-reference/projectsjobs/pause-job) for programmatic pause and resume.
  </Tab>

  <Tab title="CLI">
    <Note>
      Run from your project directory (where Intuned Settings is located), or specify the project explicitly with `-p <project-name>`.
    </Note>

    Pause a Job:

    ```bash theme={null}
    intuned platform jobs pause <job-id>
    ```

    Resume a paused Job:

    ```bash theme={null}
    intuned platform jobs resume <job-id>
    ```

    See [Jobs Pause](/docs/main/05-references/cli/platform#jobs-pause) and [Jobs Resume](/docs/main/05-references/cli/platform#jobs-resume) for all options.
  </Tab>
</Tabs>

### Terminate a JobRun

Terminating immediately stops a specific JobRun instance. All in-progress Runs are canceled, and no further payload items execute. Use terminate when you need to stop a JobRun completely—for example, if it was triggered by mistake or is no longer needed.

<Tabs>
  <Tab title="Dashboard">
    <Steps>
      <Step title="Find the JobRun">
        Navigate to the **Jobs** tab and select a Job with active JobRuns.
      </Step>

      <Step title="Terminate the JobRun">
        Select **...** next to the active JobRun, then select **Terminate**.
      </Step>
    </Steps>
  </Tab>

  <Tab title="API">
    See the [Jobs API
    reference](/docs/client-apis/api-reference/projectsjobsruns/terminate-job-run) for
    programmatic JobRun termination.
  </Tab>

  <Tab title="CLI">
    <Note>
      Run from your project directory (where Intuned Settings is located), or specify the project explicitly with `-p <project-name>`.
    </Note>

    ```bash theme={null}
    intuned platform jobruns terminate <job-run-id>
    ```

    See [Job Runs Terminate](/docs/main/05-references/cli/platform#jobruns-terminate) for all options.
  </Tab>
</Tabs>

### Delete a Job

Deleting a Job removes it permanently from your Project. Any in-progress JobRuns will be terminated. You cannot undo this action.

<Tabs>
  <Tab title="Dashboard">
    <Steps>
      <Step title="Navigate to Jobs">
        Go to the **Jobs** tab in your Project.
      </Step>

      <Step title="Delete the Job">
        Select **...** next to the Job, then select **Delete**.
      </Step>

      <Step title="Confirm deletion">Confirm when prompted.</Step>
    </Steps>
  </Tab>

  <Tab title="API">
    See the [Jobs API
    reference](/docs/client-apis/api-reference/projectsjobs/delete-job) for
    programmatic Job deletion.
  </Tab>

  <Tab title="CLI">
    <Note>
      Run from your project directory (where Intuned Settings is located), or specify the project explicitly with `-p <project-name>`.
    </Note>

    ```bash theme={null}
    intuned platform jobs delete <job-id>
    ```

    The CLI asks for confirmation before deleting. Pass `-y` / `--yes` to skip the prompt in scripts:

    ```bash theme={null}
    intuned platform jobs delete <job-id> --yes
    ```

    See [Jobs Delete](/docs/main/05-references/cli/platform#jobs-delete) for all options.
  </Tab>
</Tabs>

### Extend Run timeout

Jobs have a `requestTimeout` configuration that controls how long each Run Attempt can take before it fails. The default is 600 seconds (10 minutes). For long-running automations, call `extendTimeout` to reset the timer:

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { extendTimeout } from "@intuned/runtime";

  export default async function handler(params: any, page: Page) {
      extendTimeout(); // reset timer to the defined requestTimeout value
      // Perform long-running scraping...
      return { success: true };
  }
  ```

  ```python Python theme={null}
  from runtime_helpers import extend_timeout

  async def automation(page: Page, params: Dict[str, Any], **_ignore_kwargs):
      extend_timeout() # reset timer to the defined requestTimeout value
      # Perform long-running scraping...
      return { "success": True }
  ```
</CodeGroup>

## Use Jobs with AuthSessions

Jobs fully support AuthSessions. When you configure a Job for an authenticated Project, specify the AuthSession in the Job configuration. All Runs in the JobRun use the same AuthSession, including extended Runs added via `extendPayload`.

Jobs validate the AuthSession when the JobRun starts and before each Run attempt. If validation fails and can't recover, the JobRun pauses—you can fix the AuthSession manually and resume it from where it paused.

<Tabs>
  <Tab title="Dashboard">
    When creating or editing a Job, select the AuthSession from the **Auth Session** field.

    <Frame>
      <img alt="Add Auth To Job" />
    </Frame>
  </Tab>

  <Tab title="API">
    Include `auth_session` in the request body when [creating](/docs/client-apis/api-reference/projectsjobs/create-job) or updating a Job:

    ```json theme={null}
    {
      "auth_session": {
        "id": "auth-session-123",
        "checkAttempts": 3,
        "createAttempts": 2
      }
    }
    ```

    See the [Create Job API endpoint](/docs/client-apis/api-reference/projectsjobs/create-job) for the full request schema.
  </Tab>

  <Tab title="CLI">
    <Note>
      Run from your project directory (where Intuned Settings is located), or specify the project explicitly with `-p <project-name>`.
    </Note>

    Pass `auth_session` as part of the Job configuration:

    ```bash theme={null}
    intuned platform jobs create '{
      "id": "authenticated-scraper",
      "payload": [{"apiName": "scrape", "parameters": {}}],
      "configuration": {"retry": {"maximumAttempts": 3}},
      "auth_session": {
        "id": "auth-session-123"
      }
    }'
    ```

    See [Jobs Create](/docs/main/05-references/cli/platform#jobs-create) for all options.
  </Tab>

  <Tab title="Code">
    Add `auth_session` to your job configuration file in `intuned-resources/jobs/`:

    ```jsonc intuned-resources/jobs/authenticated-scraper.job.jsonc theme={null}
    {
      "configuration": {
        "retry": {
          "maximumAttempts": 3
        }
      },
      "payload": [
        {
          "apiName": "scrape",
          "parameters": {}
        }
      ],
      "auth_session": {
        "id": "auth-session-123"
      }
    }
    ```

    <Note>
      Jobs use `auth_session` (snake\_case) in configuration files. In TypeScript, runs use `authSession` (camelCase) instead; Python uses `auth_session` for both.
    </Note>
  </Tab>
</Tabs>

<Tip>
  For detailed information about AuthSessions, authentication patterns, and how
  validation works, see the [AuthSessions documentation](./auth-sessions) and
  [Intuned
  indepth](/docs/main/01-learn/deep-dives/intuned-indepth#authsessions-with-jobs).
</Tip>

## Extend Jobs dynamically

Jobs support **nested scheduling**, where an API can dynamically extend the JobRun's payload during execution using the `extendPayload` function. This is useful when the full scope of work isn't known until you start executing—for example, scraping an e-commerce site where product URLs aren't known upfront:

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { extendPayload } from "@intuned/runtime";
  import { Page } from "playwright";

  export default async function run(params: any, page: Page) {
    await page.goto('https://example.com/products');
    
    const productLinks = await page.$$eval('a.product', links => 
      links.map(a => a.href)
    );
    
    for (const link of productLinks) {
      extendPayload({
        api: "scrape-product-details",
        parameters: { productUrl: link }
      });
    }
    
    return {
      discoveredProducts: productLinks.length,
      message: `Extended job with ${productLinks.length} product scraping tasks`
    };
  }
  ```

  ```python Python theme={null}
  from intuned_runtime import extend_payload
  from playwright.async_api import Page
  from typing import Any, Dict

  async def automation(page: Page, params: Dict[str, Any], **_ignore_kwargs):
      await page.goto('https://example.com/products')

      product_links = await page.eval_on_selector_all('a.product',
          '(links) => links.map(a => a.href)'
      )

      for link in product_links:
          extend_payload({
              "api": "scrape-product-details",
              "parameters": { "productUrl": link }
          })

      return {
          "discoveredProducts": len(product_links),
          "message": f"Extended job with {len(product_links)} product scraping tasks"
      }
  ```
</CodeGroup>

**Important considerations:**

* `extendPayload` only works within JobRuns (not standalone Runs)
* Extended items are added to the same JobRun and tracked together
* Extended items respect the Job's retry and concurrency configuration
* Extended items automatically inherit the JobRun's AuthSession
* You can call `extendPayload` multiple times within a single API execution

## Configuration reference

Job configuration is defined using a JSON schema. The platform dashboard provides a guided wizard to help you set these values through the UI, or you can use the API to send the configuration directly.

### Basic Job structure

Every Job requires an ID and payload. Here's a minimal example:

```json theme={null}
{
  "id": "daily-book-scraper",
  "payload": [
    {
      "api": "scrape-category",
      "parameters": {
        "category": "Poetry"
      }
    },
    {
      "api": "scrape-category",
      "parameters": {
        "category": "Travel"
      }
    }
  ],
  "configuration": {
    "retry": {
      "maximumAttempts": 3
    },
    "maximumConcurrentRequests": 1
  }
}
```

### Payload configuration

The `payload` array defines what APIs to execute:

```json theme={null}
"payload": [
  {
    "apiName": "login-and-extract",
    "parameters": {
      "username": "user@example.com",
      "targetUrl": "https://example.com/dashboard"
    },
    "retry": {
        "maximumAttempts": 5
    }
  }
]
```

| Field              | Description                                             |
| ------------------ | ------------------------------------------------------- |
| `apiName`          | The name of the API to run (must exist in your Project) |
| `parameters`       | Parameters to pass to the API                           |
| `retry` (optional) | Override Job-level retry setting for this payload item  |

### Execution configuration

```json theme={null}
"configuration": {
  "retry": {
    "maximumAttempts": 3
  },
  "maximumConcurrentRequests": 10,
  "requestTimeout": 600
}
```

| Field                       | Default         | Description                                         |
| --------------------------- | --------------- | --------------------------------------------------- |
| `maximumAttempts`           | 3               | Default maximum attempts for each payload item      |
| `maximumConcurrentRequests` | Project default | Maximum payload items executing simultaneously      |
| `requestTimeout`            | 600             | Seconds to wait for each Run attempt before failing |

### Schedule configuration

Jobs support two scheduling methods:

**Intervals** — Run every X period:

```json theme={null}
"schedule": {
  "intervals": [
    { "every": "1h" },
    { "every": "86400000" }
  ]
}
```

Intervals can be milliseconds (number) or [ms-formatted strings](https://github.com/vercel/ms) like `"1h"`, `"30m"`, `"7d"`.

**Calendars** — Run at specific times:

```json theme={null}
"schedule": {
  "calendars": [
    {
      "dayOfWeek": { "start": "MONDAY", "end": "FRIDAY" },
      "hour": { "start": 9, "end": 17 },
      "minute": "0",
      "comment": "Run weekdays 9am-5pm at the start of every hour"
    }
  ]
}
```

Calendar fields support single values (`"hour": 9`), ranges (`"hour": { "start": 9, "end": 17 }`), and wildcards (`"month": "*"`).

<Info>
  Jobs trigger when **any** interval or calendar condition is met. If you
  configure both "every 7 days" and "first of every month", the Job runs when
  either condition occurs.
</Info>

### Sink configuration

Send Job results to external systems. See the [Sinks API reference](/docs/client-apis/api-reference/sinks/overview) for detailed options.

**Webhook:**

```json theme={null}
"sink": {
  "type": "webhook",
  "url": "https://webhook.site/demo"
}
```

**S3:**

```json theme={null}
"sink": {
  "type": "s3",
  "bucket": "job-results",
  "region": "us-east-1",
  "skipOnFail": false,
  "accessKeyId": "XXXXXXXXXXXXXXXXXXXX",
  "secretAccessKey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}
```

### Notifications configuration

Use `notifications` to receive a JobRun-level webhook when a JobRun reaches a terminal state.

Notifications are different from `sink`:

* `sink` sends payload-level results (many records per JobRun).
* `notifications` sends one JobRun-level summary event when the JobRun finishes, fails, is terminated, or is paused.

```json theme={null}
"notifications": [
  {
    "type": "webhook",
    "url": "https://example.com/job-run-events",
    "headers": {
      "Authorization": "Bearer <token>"
    }
  }
]
```

| Field     | Type    | Description                                                       |
| --------- | ------- | ----------------------------------------------------------------- |
| `type`    | string  | Notification type. Currently only `webhook` is supported          |
| `url`     | string  | Webhook endpoint to call when the JobRun reaches a terminal state |
| `headers` | object? | Optional headers included in the webhook request                  |

Webhook body example:

```json theme={null}
{
  "id": "jr_abc123def456ghi789",
  "projectId": "proj_abc123",
  "workspaceId": "ws_abc123",
  "jobId": "daily-sync",
  "status": "COMPLETED",
  "summary": {
    "successfulRuns": 15,
    "failedRuns": 0,
    "error": null,
    "reason": null
  },
  "aggregatedResults": {
    "signedUrl": "https://s3.amazonaws.com/...",
    "signedUrlExpiration": "2026-01-01T00:00:00.000Z",
    "size": 18293,
    "format": "jsonl",
    "key": "job-runs/.../results.jsonl",
    "bucket": "intuned-job-runs-results"
  }
}
```

<Tip>
  Configure your notification endpoint to be idempotent. Webhook notifications
  may be retried on transient failures.
</Tip>

### AuthSession configuration

<Note>
  Jobs use `auth_session` (snake\_case) in all languages and SDKs, including
  TypeScript. This is different from runs, which use `authSession` (camelCase)
  in TypeScript.
</Note>

```json theme={null}
"auth_session": {
  "id": "auth-session-123",
  "checkAttempts": 3,
  "createAttempts": 2
}
```

| Field            | Default | Description                               |
| ---------------- | ------- | ----------------------------------------- |
| `id` (required)  | —       | The ID of a credential-based AuthSession  |
| `checkAttempts`  | 3       | Times to validate before each Run attempt |
| `createAttempts` | 3       | Times to recreate if invalid              |

## Job result file schema

When a JobRun completes, Intuned stores the results as a **JSONL file** (JSON Lines — one JSON object per line). Each line represents the result of a single Run (one payload item execution). You can download this file via the dashboard or the [Get Job Run API](/docs/client-apis/api-reference/projectsjobsruns/get-job-run), which returns a pre-signed S3 URL.

### File format

**Format:** JSONL (`.jsonl`) — each line is a valid JSON object\
**Encoding:** UTF-8\
**Ordering:** Lines are written as Runs complete; order is not guaranteed

### Line schema

Each line in the file is a JSON object with the following structure:

```json theme={null}
{
  "apiInfo": {
    "name": "scrape-product",
    "parameters": { "productUrl": "https://example.com/products/123" },
    "runId": "ru_abc123def456",
    "result": { "title": "Widget", "price": 29.99 },
    "started_at": "2024-01-01T09:00:00.000Z",
    "ended_at": "2024-01-01T09:00:02.341Z",
    "error": null,
    "reason": null,
    "trace_url": "https://traces.intunedhq.com/...",
    "log_url": "https://logs.intunedhq.com/..."
  },
  "workspaceId": "ws_abc123",
  "project": {
    "id": "proj_abc123",
    "name": "my-project"
  },
  "projectJob": { "id": "job-daily-sync" },
  "projectJobRun": { "id": "jr_abc123def456ghi789" },
  "authSession": "auth-session-123"
}
```

### Field reference

#### Top-level fields

| Field              | Type    | Description                                        |
| ------------------ | ------- | -------------------------------------------------- |
| `apiInfo`          | object  | Result payload for this individual Run — see below |
| `workspaceId`      | string  | ID of the workspace                                |
| `project.id`       | string  | ID of the project                                  |
| `project.name`     | string  | Name of the project                                |
| `projectJob.id`    | string  | ID of the Job                                      |
| `projectJobRun.id` | string  | ID of the JobRun this result belongs to            |
| `authSession`      | string? | ID of the AuthSession used, if any                 |

#### `apiInfo` fields

| Field        | Type               | Description                                                                          |
| ------------ | ------------------ | ------------------------------------------------------------------------------------ |
| `name`       | string             | The API name that was executed                                                       |
| `parameters` | object?            | Input parameters passed to the API                                                   |
| `runId`      | string             | Unique ID for this Run (`ru_...`)                                                    |
| `result`     | any                | The output returned by your API. `null` if the Run failed                            |
| `started_at` | string (ISO 8601)? | When the Run started                                                                 |
| `ended_at`   | string (ISO 8601)? | When the Run ended                                                                   |
| `error`      | object?            | Error details if the Run failed — see [Error codes](/docs/main/05-references/error-codes) |
| `reason`     | object?            | Reason for early termination — see [Reason codes](/docs/main/05-references/reason-codes)  |
| `trace_url`  | string (URL)?      | Link to the browser trace for this Run                                               |
| `log_url`    | string (URL)?      | Link to the execution logs for this Run                                              |

#### `error` object

Present when a Run fails. `null` on success.

| Field            | Type    | Description                                                                 |
| ---------------- | ------- | --------------------------------------------------------------------------- |
| `message`        | string  | Human-readable error description                                            |
| `code`           | string? | Machine-readable error code (e.g. `script-timeout`)                         |
| `category`       | string  | Error category: `infrastructure`, `execution`, `auth`, `user`, or `billing` |
| `doc_url`        | string? | Link to documentation for this error                                        |
| `correlation_id` | string? | Correlation ID for tracing in logs                                          |

#### `reason` object

Present when a Run was stopped for a non-error reason (e.g. job terminated). `null` otherwise.

| Field     | Type    | Description                                       |
| --------- | ------- | ------------------------------------------------- |
| `type`    | string  | Reason type (e.g. `terminated`, `job-run-paused`) |
| `message` | string  | Human-readable description                        |
| `doc_url` | string? | Link to documentation for this reason             |

### Reading the result file

<CodeGroup>
  ```typescript TypeScript theme={null}
  import * as fs from "fs";
  import * as readline from "readline";

  const file = fs.createReadStream("job-results.jsonl");
  const rl = readline.createInterface({ input: file });

  rl.on("line", (line) => {
    const record = JSON.parse(line);
    const { name, runId, result, error } = record.apiInfo;

    if (error) {
      console.error(`Run ${runId} failed: ${error.message}`);
    } else {
      console.log(`Run ${runId} (${name}):`, result);
    }
  });
  ```

  ```python Python theme={null}
  import json

  with open("job-results.jsonl", "r") as f:
      for line in f:
          record = json.loads(line)
          api_info = record["apiInfo"]

          if api_info.get("error"):
              print(f"Run {api_info['runId']} failed: {api_info['error']['message']}")
          else:
              print(f"Run {api_info['runId']} ({api_info['name']}): {api_info['result']}")
  ```
</CodeGroup>

<Tip>
  A Run with `result: null` and a non-null `error` field indicates a failed execution. A Run with `result: null` and `error: null` means the API returned `null` intentionally.
</Tip>

## Best practices

* **Keep APIs focused**: Design each API to handle a single concern. Use Jobs to orchestrate multiple APIs rather than building monolithic automations.

* **Use nested scheduling for discovery patterns**: When scraping lists before details, use one API to discover items and `extendPayload` to process each.

* **Limit concurrency for rate-limited targets**: Set `maximumConcurrentRequests` to 1-5 for rate-limited sites. Increase for robust targets.

* **Include metadata in parameters**: Pass identifiers or context (`{ "category": "electronics", "batchId": "2024-10-16" }`) for easier debugging.

* **Use sinks for production workflows**: Configure webhooks or S3 to automatically capture results rather than manually exporting.

* **Test before scheduling**: Create a QA instance of your Job (no schedule or sink) to test manually before setting up the production version.

* **Use service account AuthSessions**: Use dedicated service accounts rather than personal credentials for clearer audit trails.

* **Monitor JobRun history regularly**: Check for patterns in failures. Consistent failures in specific payload items may indicate API issues.

## Limitations

* **Execution order is not guaranteed**: Payload items may execute in any order depending on concurrency and worker availability.

* **Extended payload items execute asynchronously**: Items added via `extendPayload` are queued and execute as workers become available.

* **No conditional execution within Jobs**: Jobs execute all payload items. Use nested scheduling and API-level logic for conditional workflows.

* **Schedule precision**: Scheduled JobRuns trigger within a reasonable window but not at the exact millisecond.

* **Sink and notification delivery are *at-least-once***: Results and JobRun notifications may be delivered multiple times in rare failure scenarios. Handle duplicates idempotently.

* **AuthSession is shared across all Runs**: You cannot use different AuthSessions for different payload items within the same JobRun.

* **Only credential-based AuthSessions support auto-recreation**: Recorder-based AuthSessions must be manually recreated when they expire.

## FAQs

<AccordionGroup>
  <Accordion title="What's the difference between Jobs and direct Run API calls?">
    Direct Run API calls execute a single API immediately. Jobs orchestrate multiple API calls together with scheduling, retries, and concurrency management. Use direct Runs for one-off executions; use Jobs for batch processing and recurring automations.
  </Accordion>

  <Accordion title="Can I modify a Job's payload after creating it?">
    Yes. Changes apply to future JobRuns—they don't affect JobRuns currently in
    progress.
  </Accordion>

  <Accordion title="How do I pass different parameters to the same API multiple times?">
    Include multiple payload items with the same `api` value but different
    `parameters`. Each creates a separate Run.
  </Accordion>

  <Accordion title="What happens if my API calls extendPayload multiple times?">
    Each call adds items to the current JobRun's queue. All extended items are
    tracked together and execute according to the Job's concurrency and retry
    settings.
  </Accordion>

  <Accordion title="Can I use different AuthSessions for different APIs in the same Job?">
    No. All Runs within a JobRun use the same AuthSession. Create separate Jobs
    for different AuthSessions.
  </Accordion>

  <Accordion title="How does authentication work with nested scheduling?">
    Extended Runs automatically inherit the JobRun's AuthSession. You don't need
    to specify authentication for extended payload items.
  </Accordion>

  <Accordion title="What happens if a scheduled Job triggers while a previous JobRun is still running?">
    The new scheduled JobRun will not start while an older JobRun is still in progress. Intuned prevents overlapping JobRuns to avoid resource conflicts and ensure predictable execution. Once the running JobRun completes, the next scheduled trigger will start normally. If you need parallel execution, consider splitting your Job into multiple Jobs with different parameters or increasing concurrency within a single JobRun.
  </Accordion>
</AccordionGroup>

## Related resources

<CardGroup>
  <Card title="AuthSessions" icon="key" href="./auth-sessions">
    Authenticated browser automations
  </Card>

  <Card title="Jobs API reference" icon="code" href="/client-apis/api-reference/projectsjobs/get-jobs">
    Programmatic Job management
  </Card>

  <Card title="Sinks API reference" icon="webhook" href="/client-apis/api-reference/sinks/overview">
    Webhook and S3 result delivery
  </Card>

  <Card title="Monitoring and traces" icon="chart-line" href="./observability-monitoring-logs">
    Debug and monitor Job executions
  </Card>
</CardGroup>


# Local development
Source: https://intunedhq.com/docs/main/02-features/local-development-cli



Develop any Intuned Project locally with the CLI and your preferred editor.

Use local development when:

* You prefer your own IDE instead of Intuned's online IDE.
* You want to manage the project with version control (e.g., Git) for better collaboration and versioning.
* You want to use your own development tools, extensions, and AI tools.
* You prefer working in your own environment, with or without Intuned Agent (see the [External agent](/docs/main/02-intuned-agent/external-agent) plugin).

For the browser-based approach, see [Online IDE](./online-ide).

<Tip>
  Want the agent in your local setup too? The [External
  agent](/docs/main/02-intuned-agent/external-agent) plugin (private preview,
  available on request) runs Intuned Agent locally to build, edit, and debug
  projects.
</Tip>

## Install the CLI

Install the Intuned CLI by following the [CLI installation guide](/docs/main/02-features/cli#installation).

## Create a project

To create a new Intuned project locally, run:

<CodeGroup>
  ```txt CLI theme={null}
  intuned dev init
  ```

  ```txt npx theme={null}
  npx create-intuned-project@latest
  ```
</CodeGroup>

<Note>
  Requires Node.js and npm. Run with `-h` to see all available options.
</Note>

The command walks you through project creation: selecting a language (TypeScript or Python), choosing a starter template, setting the project name, and connecting to your workspace.

Once the project is created, you can expect the following structure:

<CodeGroup>
  ```typescript theme={null}
  // api
  // └── ...
  // auth-sessions (if AuthSessions are enabled)
  // └── check.ts
  // └── create.ts
  // auth-sessions-instances (if AuthSessions are enabled)
  // ├── test-authsession/
  // └── ...
  // intuned-resources
  // ├── auth-sessions/ (if AuthSessions are enabled)
  // │   └── test-authsession.auth-session.jsonc
  // └── jobs/
  //     └── default.job.jsonc
  // Intuned.jsonc (Intuned settings file)
  // package.json (dependencies)
  // README.md
  // tsconfig.json
  // yarn.lock
  ```

  ```python theme={null}
  #  api
  #  └── ...
  #  auth-sessions (if AuthSessions are enabled)
  #  └── check.py
  #  └── create.py
  #  auth-sessions-instances (if AuthSessions are enabled)
  #  ├── test-authsession/
  #  └── ...
  #  intuned-resources
  #  ├── auth-sessions/ (if AuthSessions are enabled)
  #  │   └── test-authsession.auth-session.jsonc
  #  └── jobs/
  #      └── default.job.jsonc
  #  Intuned.jsonc (Intuned settings file)
  #  pyproject.toml (dependencies)
  #  README.md
  #  uv.lock
  #
  ```
</CodeGroup>

### Intuned settings file (Intuned.jsonc)

The Intuned settings file contains the configuration settings for your project, including AuthSessions, replication settings, and more. For local projects, this includes your **workspace ID** and **project name**.

Workspace ID and project name are required to save and deploy your project to Intuned. If provided during project creation, they're saved in the Intuned settings file automatically. Otherwise, add them manually later.

```jsonc Intuned.jsonc theme={null}
{
  "projectName": "<your-project-name>",
  "workspaceId": "<your-workspace-id>"
  // ...
}
```

Or pass them to each command as arguments. Run the respective command with `-h` to see the available options.

<Note>By default, the Intuned settings file is `Intuned.jsonc` (JSON with comments). If you want to use a different format, you can pass `--settings-format <settings-format>` to the create command. For more info, run with `-h`.</Note>

For more information on the settings file and its options, refer to [Intuned settings file](../05-references/intuned-json).

## Authenticate

Authenticate to Intuned by following the [CLI authentication guide](/docs/main/02-features/cli#authentication). This allows you to save and deploy your project, and use CLI features that require authentication.

## Local development workflow

Use any IDE or text editor to develop your project locally. Start by installing your project's dependencies.

<Tip>
  The recommended dependency managers are `yarn` (v1) for TypeScript projects
  and `uv` for Python projects.
</Tip>

### Understand the project structure

The `api` directory contains your browser automation APIs. Create new APIs by adding new files in this directory, which can be nested. An example for a social media RPA project:

<CodeGroup>
  ```typescript theme={null}
  // api/
  // ├── messages/
  // │   ├── send.ts
  // │   └── react.ts
  // └── posts/
  //     ├── react.ts
  //     ├── reply.ts
  //     └── share.ts

  // Available APIs:
  //  messages/send
  //  messages/react
  //  posts/react
  //  posts/reply
  //  posts/share
  ```

  ```python theme={null}
  #  api/
  #  ├── messages/
  #  │   ├── send.py
  #  │   └── react.py
  #  └── posts/
  #      ├── react.py
  #      ├── reply.py
  #      └── share.py
  #
  #  Available APIs:
  #   messages/send
  #   messages/react
  #   posts/react
  #   posts/reply
  #   posts/share
  ```
</CodeGroup>

### Write APIs

Implement your APIs using any functions, classes, or design patterns you prefer. The entry point for each API is defined as follows:

<CodeGroup>
  ```typescript api/example.ts theme={null}
  // The default export is the entry point for the API

  export default async function automation(
    params: any,
    page: Page,
    context: BrowserContext
  ) {
    // Your automation code here
    return {}; // Return results
  }
  ```

  ```python api/example.py theme={null}
  # The function named `automation` is the entry point for the API

  async def automation(
      page: Page,
      params: Any | None = None,
      **kwargs
  ):
      # Your automation code here
      return {} # Return results
  ```
</CodeGroup>

Define other directories and files to import from and use in your APIs as needed.

### Run APIs

Run an API:

<CLICommandTabs />

<Tip>
  Run with `-h` or check out [CLI Reference](../05-references/cli/overview) to
  see all available options.
</Tip>

### Debug APIs

If the API isn't running as expected, use the `--keep-browser-open` flag to keep the browser open for debugging. Use the `--trace` flag to generate a trace, which you can open with Playwright's trace viewer.

Open the trace with:

```
npx playwright show-trace <trace-file>
```

### Work with AuthSessions

AuthSessions are reusable authentication sessions shared across multiple APIs. They manage authentication flows and store session data securely. For more information, refer to [AuthSessions (authenticated automation)](./auth-sessions).

<Note>
  If you use a starter template that includes AuthSessions, the project comes
  with an AuthSession instance called `test-authsession` that you can find under
  the `auth-sessions-instances` directory.
</Note>

#### AuthSession directories

During local development, projects using AuthSessions have two additional directories:

* `auth-sessions`: Contains the create and check functions for AuthSessions.
* `auth-session-instances`: Contains the stored AuthSession instances. Each instance is stored in a separate directory with its ID as the directory name.

<Note>
  The `auth-session-instances` directory is only used during local development.
  When deployed, AuthSession data is stored securely and encrypted on Intuned's
  infrastructure.
</Note>

<CodeGroup>
  ```typescript theme={null}
  // auth-sessions/
  // ├── create.ts
  // └── check.ts
  // auth-session-instances/
  // ├── test-authsession/ # test-authsession is an id for an AuthSession
  // └── auth-session-2/    # auth-session-2 is an id for an AuthSession
  ```

  ```python theme={null}
  # auth-sessions/
  # ├── create.py
  # └── check.py
  # auth-session-instances/
  # ├── test-authsession/ # test-authsession is an id for an AuthSession
  # └── auth-session-2/    # auth-session-2 is an id for an AuthSession
  ```
</CodeGroup>

#### Write the create function

Inside `auth-sessions/create`, write code that creates your AuthSession (e.g., logging in). The create function doesn't return anything—the browser state is captured after it completes.

<CodeGroup>
  ```typescript auth-sessions/create.ts theme={null}
  export default async function create(
    page: Page,
    params: any,
    context: BrowserContext
  ) {
    // Your authentication code here
  }
  ```

  ```python auth-sessions/create.py theme={null}
  async def create(
      page: Page,
      params: Any | None = None,
      **kwargs
  ):
      # Your authentication code here
      ...
  ```
</CodeGroup>

#### Write the check function

Inside `auth-sessions/check`, write code to verify the AuthSession is valid (e.g., checking the profile page to confirm you're logged in). The check function takes no parameters and returns a boolean indicating whether the session is valid.

<CodeGroup>
  ```typescript auth-sessions/check.ts theme={null}
  export default async function check(
    page: Page,
    context: BrowserContext
  ): Promise<boolean> {
    // Your session validation code here
    return true; // Return true if valid, false otherwise. Error will also be treated as invalid.
  }
  ```

  ```python auth-sessions/check.py theme={null}
  async def check(
      page: Page,
      **kwargs
  ) -> bool:
      # Your session validation code here
      return True  # Return True if valid, False otherwise. Exception will also be treated as invalid.
  ```
</CodeGroup>

#### Manage AuthSessions

**Create an AuthSession:**

<CLICommandTabs />

**Validate an AuthSession:**

<CLICommandTabs />

**Update an existing AuthSession:**

<CLICommandTabs />

<Tip>
  Run with `-h` or check out the [CLI Reference](../05-references/cli/overview)
  to see all available options.
</Tip>

#### Run APIs with AuthSessions

When AuthSessions are enabled, APIs require an AuthSession ID to run:

<CLICommandTabs />

### Use Runtime SDK and browser SDK helpers

Some helpers in the Intuned Runtime SDK and Browser SDK need backend resources that are only available after you provision your project. Without provisioning, you'll see one of these messages:

```
Unauthorized backend function call - make sure to provision your project to Intuned to set up the correct API credentials

API credentials not set - make sure to provision your project to Intuned to set up the correct API credentials.
```

Resolve this by provisioning the project, then saving its local files:

<CLICommandTabs />

<CLICommandTabs />

<Note>
  If you don't have your project name and workspace ID set in the Intuned
  settings file, provide them as arguments/options.
</Note>

## Deploy your project

When your project is ready, deploy it. The command saves local changes before starting the deployment:

<CLICommandTabs />

After deployment succeeds, follow the output links for next steps.

Use `--no-save` only when the current project on Intuned is already the version you want to deploy. Saves block by default if the local checkout is stale or changes conflict. In automation, opt into merging and local conflict resolution explicitly with `--on-stale merge --on-conflict override`.

<Note>
  If you don't have your project name and workspace ID set in the Intuned
  settings file, provide them as arguments/options.
</Note>

### Set up CI/CD

Integrate Intuned deployment into your CI/CD pipelines with the CLI. Set `INTUNED_API_KEY` in your CI/CD environment.

<CodeGroup>
  ```yaml GitHub Actions (TypeScript) icon="github" theme={null}
  name: Deploy to Intuned
  on:
    push:
      branches:
        - main
  jobs:
    deploy:
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v2
        - name: Set up Node.js
          uses: actions/setup-node@v2
          with:
            node-version: '24'
        - name: Install Intuned CLI
          run: npm install -g @intuned/cli
        - name: Install dependencies
          run: yarn install
        - name: Deploy to Intuned
          env:
            INTUNED_API_KEY: ${{ secrets.INTUNED_API_KEY }}
          run: intuned dev deploy --non-interactive --on-stale merge --on-conflict override
  ```

  ```yaml GitHub Actions (Python) icon="github" theme={null}
  name: Deploy to Intuned
  on:
    push:
      branches:
        - main
  jobs:
    deploy:
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v2
        - name: Set up Node.js
          uses: actions/setup-node@v2
          with:
            node-version: "24"
        - name: Set up Python
          uses: actions/setup-python@v5
          with:
            python-version: "3.12"
        - name: Install uv
          run: pip install uv
        - name: Install Intuned CLI
          run: npm install -g @intuned/cli
        - name: Install dependencies
          run: uv sync
        - name: Deploy to Intuned
          env:
            INTUNED_API_KEY: ${{ secrets.INTUNED_API_KEY }}
          run: intuned dev deploy --non-interactive --on-stale merge --on-conflict override
  ```
</CodeGroup>

## Choose an online or local workflow

| Feature             | Online IDE                                 | Local CLI                                      |
| ------------------- | ------------------------------------------ | ---------------------------------------------- |
| **Setup**           | Browser-based with no local setup          | Requires Python or Node.js and the Intuned CLI |
| **Platform AI**     | Built into the project workflow            | Available after syncing the project to Intuned |
| **Intuned Agent**   | Run from Intuned                           | Run with the external agent plugin             |
| **Version control** | Intuned checkpoints and deployment history | Your Git repository and CI/CD workflow         |
| **IDE choice**      | Intuned's web editor                       | Your preferred local editor                    |

Every project supports both workflows. Sync Intuned changes before saving from a local checkout.

## Related resources

<CardGroup>
  <Card title="CLI reference" icon="terminal" href="/main/05-references/cli/overview">
    Complete CLI command documentation
  </Card>

  <Card title="How the platform works" icon="book" href="/main/00-getting-started/how-intuned-works">
    Understand Intuned's architecture and execution model
  </Card>

  <Card title="AuthSessions" icon="key" href="/main/02-features/auth-sessions">
    Build authenticated automations
  </Card>

  <Card title="Online IDE" icon="browser" href="/main/02-features/online-ide">
    Build automations with zero setup
  </Card>
</CardGroup>


# Monitoring and traces
Source: https://intunedhq.com/docs/main/02-features/observability-monitoring-logs



## Overview

Intuned provides full visibility into your automation Runs. See what parameters were used, what results came back, and what failed.

Track executions at three levels:

* **Runs**: Individual API executions with inputs, outputs, and status
* **Attempts**: Each retry within a Run, with logs and traces
* **JobRuns**: Batch executions containing multiple Runs

## View Run records

Every time you execute an API in Intuned, a **Run** record is created. A Run represents a single logical execution and tracks:

* Input parameters and configuration (timeouts, retries, auth settings)
* Execution status and timing
* Output results or error details
* All retry attempts made during execution

<img alt="Screenshot of the Runs table showing multiple Run records with status, duration, and error columns" />

1. Find the Runs table in the **Runs** section of the project dashboard
2. Each Run row shows a single API execution with its status, timing, duration, and more
3. Scan failed Runs by checking the status and the related error/reason column. Check more about error types in our [error code reference](/docs/main/05-references/error-codes)
4. Use the filters at the top to narrow down Runs by status, date range, job, API, and more

<Tip>
  Use the Run ID to fetch Run details programmatically via the [Run result API](/docs/client-apis/api-reference/projectsruns/run-api--result).
</Tip>

<Tip>
  Use the CLI to list and inspect runs without opening the dashboard. Run from your project directory or use `-p <project-name>`:

  ```bash theme={null}
  # List recent runs
  intuned platform runs list

  # Filter by status or API name
  intuned platform runs list --filter status=FAILED
  intuned platform runs list --filter api_name=my-api

  # Get details for a specific run
  intuned platform runs get <run-id>
  ```

  See [Runs List](/docs/main/05-references/cli/platform#runs-list) and [Runs Get](/docs/main/05-references/cli/platform#runs-get) for all options.
</Tip>

### View Run details

Click any Run record from the table to open the Run details page. This page shows details about that specific API execution.

<img alt="Screenshot of Run details page showing metadata, input/output panels, and attempts timeline" />

1. **Metadata section**: Displays the Run's ID, status, duration, timing, associated Job and AuthSession (if any), and API name
2. **Input/Output section**: Split into two panels, this section shows the input parameters used for the Run on the left and the output results on the right. You can switch between different tabs to view results, extended payloads, errors, and more
3. **Attempts Timeline**: This visual timeline shows all attempts made for this Run, including their statuses, durations, and timestamps. Click any attempt to view more details, including logs and traces. Learn more about the [attempts and dependencies execution model](/docs/main/00-getting-started/how-intuned-works#runs-and-attempts).

### Runs with AuthSession

When a Run is configured to use an AuthSession, you'll notice additional fields and behaviors in the Run details page

<img alt="Screenshot of Run details page with AuthSession field highlighted in metadata and dependency shown in timeline" />

1. AuthSession field in the Metadata section indicates which AuthSession was used for this Run
2. `AuthSession:Validate` dependency appears in the Attempts Timeline before each API attempt to ensure the session is valid. Click it to view its own Run details. Learn more about [AuthSession](/docs/main/02-features/auth-sessions).

## Inspect Run attempts

Each Run can have multiple **Attempts**, especially if retries are enabled or if there are dependencies involved (like AuthSessions). Each attempt represents a single try to execute the API automation.

<img alt="Screenshot of Run attempts timeline showing multiple attempt entries with status and duration" />

1. View the attempt entry in the Attempts Timeline, showing API name, status, duration, and timestamp
2. Click an attempt to view detailed input parameters and output results
3. Access logs and traces for debugging and analysis

### Debug failed attempts with traces

<img alt="Screenshot of Playwright trace viewer showing step-by-step browser session replay with network and DOM inspection tools" />

When an attempt fails:

1. Select **View Trace** for the failed attempt to open Playwright's trace viewer
2. Replay the browser session step-by-step
3. Check if a bot detection mechanism was triggered
4. Inspect network requests to see if any resources failed to load
5. Check DOM snapshots to understand the page state at each step
6. Analyze failed selectors or actions that caused the error

<Tip>
  Download attempt logs and traces directly via the CLI. Run from your project directory or use `-p <project-name>`:

  ```bash theme={null}
  # Download automation logs for an attempt
  intuned platform attempts log <run-id>

  # Download browser trace (Playwright .zip) for an attempt
  intuned platform attempts trace <run-id>

  # If a run has multiple attempts, specify the attempt number
  intuned platform attempts log <run-id> 2
  intuned platform attempts trace <run-id> 2
  ```

  See [Attempts Log](/docs/main/05-references/cli/platform#attempts-log) and [Attempts Trace](/docs/main/05-references/cli/platform#attempts-trace) for all options.
</Tip>

## Monitor JobRuns

In addition to individual API Run records, Intuned provides a way to visualize and monitor the execution of entire Jobs through **JobRun** records. A JobRun represents a single execution of a Job. It may include multiple API Runs. Learn more about [Jobs](/docs/main/02-features/jobs-batched-executions).

### View JobRun details

Navigate to the **Jobs** section within your project. You'll see a list of all your defined Jobs. Click any Job to view its details.

<img alt="Animation showing navigation to Jobs section and opening a JobRun details page" />

For each Job details page, you can view the job configuration, schedule, and more. You can also:

* Edit the Job configuration
* Trigger a JobRun manually
* Pause or resume scheduled executions

Below, view the **JobRuns** with a detailed summary of all payloads executed.

<img alt="Screenshot of JobRun details showing summary information and filterable list of API Runs" />

For each JobRun, you can see:

1. A detailed section with all JobRun information like status, start time, duration, summary of payloads executed, and more
2. A filterable list of all the API Runs that were part of this JobRun, including their statuses, durations, and timestamps
3. For each API Run within the JobRun, you can click on it to view its own Run details, with the result and parameters of this specific execution

<Tip>
  Monitor JobRuns from the CLI. Run from your project directory or use `-p <project-name>`:

  ```bash theme={null}
  # List all JobRuns for a Job
  intuned platform jobruns list <job-id>

  # Get details for a specific JobRun
  intuned platform jobruns get <job-run-id>
  ```

  See [Job Runs List](/docs/main/05-references/cli/platform#jobruns-list) and [Job Runs Get](/docs/main/05-references/cli/platform#jobruns-get) for all options.
</Tip>

## Related resources

<CardGroup>
  <Card title="Error codes reference" icon="circle-exclamation" href="/main/05-references/error-codes">
    Understand error types and what they mean
  </Card>

  <Card title="Run API reference" icon="code" href="/client-apis/api-reference/projectsruns/run-api--result">
    Fetch Run details programmatically
  </Card>

  <Card title="AuthSessions" icon="key" href="/main/02-features/auth-sessions">
    Authenticated browser automations
  </Card>

  <Card title="Jobs and batched executions" icon="list-check" href="/main/02-features/jobs-batched-executions">
    Schedule and batch your automations
  </Card>
</CardGroup>


# Online IDE
Source: https://intunedhq.com/docs/main/02-features/online-ide



## Overview

The online IDE lets you develop any Intuned Project in your browser.

You should use the online IDE when:

* You want zero setup—just open your browser and start coding.
* You want embedded AI assistance to build and edit automations.
* You want Selector Agent to generate selectors from the live page.
* You need platform-native workflows like AuthSessions and browser-based debugging.
* You're working solo on smaller projects.

For local development, see [Local development](/docs/main/02-features/local-development-cli).

## Create a project

To create a project that runs in the Online IDE:

1. Go to [app.intuned.io/projects](https://app.intuned.io/projects)
2. Select **Create Project**
3. Choose your language (TypeScript or Python)
4. Select a template or start from scratch
5. Name your project
6. Select **Start in Intuned**.
7. Select **Create and Open**

<Frame>
  <img alt="Create project dialog with IDE type selected" />
</Frame>

Your new project opens immediately in the online IDE with all components ready to use.

## Online IDE interface overview

<Frame>
  <img alt="Intuned IDE layout" />
</Frame>

The Online IDE includes these components:

### 1. File Explorer (Top Left Sidebar)

The file explorer displays your project's folder structure:

<CodeGroup>
  ```typescript theme={null}
  //api/
  //└── sample.ts          # Your automation API files
  //Intuned.json           # Project configuration
  //package.json           # Package dependencies
  ```

  ```python theme={null}
  #api/
  #└── sample.py          # Your automation API files
  #Intuned.json           # Project configuration
  #pyproject.toml         # Package dependencies
  ```
</CodeGroup>

### 2. Changes to deploy (Bottom Left Sidebar)

Shows changes that will be included in your next deployment.

### 3. Code Editor (center panel)

A VS Code-based editor with syntax highlighting, IntelliSense, familiar keyboard shortcuts, embedded AI assistance for project edits, and inline code suggestions.

### 4. Run Controls (Top Center Toolbar)

Select an API, configure parameters, and start execution with the **Play** button.

### 5. Terminal (Bottom panel)

Displays execution logs, traces, and output from your automation runs:

* **OUTPUT** tab - Shows VS Code extension logs and messages
* **TERMINAL** tab - Shows command output such as `yarn install` or `intuned run api`

The terminal shows what's happening during execution—essential for debugging.

<Info>
  You can't use the terminal to run arbitrary commands. It only displays output
  from IDE-executed commands.
</Info>

### 6. Browser Panel (Right Panel)

A live browser view that shows your automation executing in real-time. Watch exactly what your code does and debug UI issues immediately.

### 7. Browser Controls (Above Browser Panel)

* **Restart Browser** - Restarts the browser instance
* **Clipboard** - Copy text from the browser to your local clipboard
* **DevTools** - Opens browser dev tools for debugging
* **Selector Agent** - Generates selectors for elements using the live page context

### 8. View Controls + Deploy Button (Top Right Toolbar)

* **Toggle Sidebar** - Show/hide the left sidebar
* **Toggle Terminal** - Show/hide the bottom terminal panel
* **Toggle Browser Panel** - Show/hide the right browser panel
* **Theme selector** - Switch between light, dark, and system themes
* **Deploy** - Triggers project deployment

## AI in the online IDE

The online IDE includes embedded AI assistance for building and maintaining automations without leaving the editor.

<Frame>
  <img alt="AI chat panel in the online IDE" />
</Frame>

### What it can do

* Answer browser automation questions in the context of your current project
* Edit the current project
* Generate selectors from live page context through Selector Agent
* Provide inline code suggestions while you type

### Selector Agent

Selector Agent is the selector-focused part of the built-in AI in the online IDE. It uses the live browser view and the current project context to generate code-ready selectors for the automation you're building.

To use Selector Agent, click the **Selector Agent** icon in the browser controls toolbar, then click the element you want to target on the live page. Selector Agent analyzes the page context and generates a reliable selector for your automation code.

<Frame>
  <img alt="Selector Agent element selection mode in the browser panel" />
</Frame>

Use Selector Agent when element targeting is unclear, the DOM is noisy, or you want a faster path than manually inspecting markup and guessing selectors.

### Availability and credits

These features consume AI credits, so if credits are exhausted the AI tools in the editor are unavailable until credits are available again.

## Development workflow

APIs in the `api/` folder become callable endpoints when deployed. Build them by editing code manually or with the built-in AI tools, use Selector Agent when you need help targeting elements on the live page, then use the run controls to test any API with parameters before deploying. The browser panel shows your automation executing in real-time, so you can verify behavior visually and iterate quickly.

<Frame>
  <img alt="Running an API in the IDE with real-time browser view" />
</Frame>

## Draft vs deployed versions

When developing in the IDE, your project exists in two states:

* **Draft version**: The code currently in your IDE editor (not yet live)
* **Deployed version**: The code running in production (available via API endpoints)

The **Changes to deploy** panel shows what changed between draft and deployed versions.

### How deployment works

When you select **Deploy**, we build and validate your code, run health checks, and deploy if all checks pass. Only the deployed version is accessible via Standalone Run APIs and Jobs—draft changes don't affect production until you deploy again.

## Branches and checkpoints

Your project's shared draft is called **Main**. Intuned gives you two tools for managing change on top of it:

* **Branches** are isolated copies of `Main` where work happens before merge.
* **Checkpoints** are saved project states created automatically when Intuned persists important code changes.

The main use case for branches is agent work. Intuned Agent uses branches to keep its changes isolated until you review and merge them. You can also create branches yourself when you want to make a change without editing `Main` directly.

### Branches

Branches are part of the Intuned workflow for IDE projects. They are not Git branches.

#### What branches are for

Use a branch when you want to:

* keep work isolated from `Main`
* review a change set before merging it
* let the agent work on a project without editing the shared draft directly
* organize active work separately from completed work

<Frame>
  <img alt="Project Home showing the Branches section with the create branch action" />
</Frame>

#### How to create a branch

Branches are managed from **Project Home**, the project overview page outside the editor.

1. Open the project in **Project Home**
2. In the **Branches** panel, select **Create Branch**
3. Enter a branch name
4. Open the branch in the IDE

New branches start from the current state of `Main`.

#### How to view a branch

You can view branches in two places:

* **Project Home** shows the full list of branches, their status, and actions such as open, merge, and archive
* **The IDE** shows which branch you are currently editing

From the Branches panel, open any branch in the IDE to continue working on it.

#### How to merge a branch

1. Make the changes on the branch
2. Mark the branch as **Ready** when the change set is ready for review
3. Return to **Project Home**
4. In the **Branches** panel, merge the branch back into `Main`

Intuned checks the current project state before merging. If a linked agent session is still in progress, merge may be temporarily unavailable until that session reaches a mergeable state.

#### Branch states

The Branches panel and the IDE use these states:

| State         | What it means                                                           |
| ------------- | ----------------------------------------------------------------------- |
| `In Progress` | Work is still happening on the branch.                                  |
| `Ready`       | The branch is ready to be merged into `Main`.                           |
| `Merged`      | The branch changes have already been merged into `Main`.                |
| `Archived`    | The branch is no longer active, but it remains part of project history. |
| `Deployed`    | Changes from the branch have been included in a deployment.             |

When the agent handles deployment, you may also see **Auto-deployed** as the user-facing label.

#### Agent-related branch badges

When the agent is involved, Project Home can show automation badges such as:

* **Auto-created**
* **Auto-merged**
* **Auto-deployed**

These badges help you understand which parts of the branch lifecycle were handled by the agent.

### Checkpoints

Checkpoints are created automatically when Intuned saves important project states. They are part of project history, not separate working spaces.

#### Which checkpoints you will usually see

You do not create checkpoints manually in the current workflow described here. Intuned creates them automatically when important project states are saved.

Checkpoint-related activity entries can appear in different forms depending on what was saved. Common examples include:

* descriptive notes attached to a saved change, such as **Adding comments to the books scraper**
* automatic **Pre-merge checkpoint** entries created before a branch is merged into `Main`
* **Merged branch** or **Deployed to production** entries when a merge or deployment is saved without a more specific note

These checkpoints record that code was saved at an important milestone.

#### How to view checkpoints

Open **Project Home** and check the **Activity** panel. This is where checkpoint-related history appears.

The Activity panel shows:

* what happened
* who or what triggered it
* when the code was saved

This makes it easy to understand how the project changed over time across both human and agent work.

<Frame>
  <img alt="Project Home activity panel showing checkpoint-related entries" />
</Frame>

<Info>
  Checkpoints in the current product are viewed through project history in the
  Activity panel. They are not a replacement for Git history or a standalone
  checkpoint manager.
</Info>

## Work with AuthSessions

For projects that require authentication (logging into websites before automation or persisting browser sessions between runs), you can enable **AuthSessions** in the project. See [AuthSessions](/docs/main/02-features/auth-sessions) for details.

### Enabling AuthSessions

1. Open `Intuned.json` in your project
2. Set `"authSessions": true` in the configuration

<Frame>
  <img alt="Enabling AuthSessions in Intuned.json" />
</Frame>

This creates special authentication files in your project:

* `auth-sessions/create` - Handles the login/authentication process
* `auth-sessions/check` - Validates if the session is still valid

### Testing authenticated APIs

Before running APIs that require authentication:

1. **Create an AuthSession** by running `auth-sessions/create`
2. **Run your API** - it will now use the newly created AuthSession
3. If you have multiple AuthSessions, select which one to use from the dropdown in the top toolbar

<Frame>
  ![Creating and validating AuthSessions in the
  IDE](https://cdn1.intuned.io/create-auth-session-ide.gif)
</Frame>

You can create AuthSessions two ways:

* **Credentials-based**: Use `auth-sessions/create` to create and save a browser session
* **Recorder-based**: Record yourself logging in via the browser panel, then save the session when done

## Deploy your project

When you're ready to make your automation available as a live API:

1. **Select Deploy** in the top-right toolbar
2. **Watch the live deployment logs** as your project is built and deployed
3. **Wait for "Ready" status** indicating successful deployment

<Info>
  If deployment fails, the IDE will show error logs. Fix the issues in your code
  and try deploying again. Your previous deployed version remains active until a
  successful deployment completes.
</Info>

After deployment, all APIs become callable endpoints. Trigger runs via [Jobs](/docs/main/02-features/jobs-batched-executions) or [API calls](/docs/main/02-features/runs-single-executions).

## Intuned settings file (Intuned.json)

The online IDE uses `Intuned.json` for configuration. Open it in the file explorer to view or edit settings via UI.

See the [Intuned settings file](/docs/main/05-references/intuned-json) for details on each configuration value.

<Frame>
  <img alt="Intuned.json configuration in the IDE" />
</Frame>

## Best practices

<AccordionGroup>
  <Accordion title="Test thoroughly before deploying">
    Always run your APIs multiple times in the IDE with different parameters to ensure they work correctly. Use the real-time browser view to catch issues early.
  </Accordion>

  <Accordion title="Use AuthSessions for authenticated workflows">
    If your automation requires logging in, enable AuthSessions and test the
    authentication flow in the IDE before deployment. Ensure `auth-sessions/check`
    passes consistently and is performant.
  </Accordion>

  <Accordion title="Leverage the Intuned Agent">
    Use [Intuned Agent](/docs/main/02-intuned-agent/overview) to create and edit projects through natural language. You can also run the agent locally with the [External agent](/docs/main/02-intuned-agent/external-agent) plugin.
  </Accordion>
</AccordionGroup>

## Limitations

**No Git integration**: The IDE doesn't integrate with Git, but you have full versioning and deployment history built in. For Git workflows, use the [Local CLI](/docs/main/02-features/local-development-cli).

**Single online editor session**: Multiple people cannot edit the same project in the online IDE simultaneously.

**No local IDE choice**: You must use Intuned's web-based editor. If you prefer VS Code, JetBrains, or other IDEs, use the CLI approach.

## FAQs

<AccordionGroup>
  <Accordion title="Can I use both the online IDE and local CLI?">
    Yes. Every project supports both workflows. Run `intuned dev checkout --project-name <project-name>` to sync Intuned changes before your next local save.
  </Accordion>

  <Accordion title="What happens to my draft if I don't deploy?">
    Your draft remains in the IDE. The deployed version continues running
    unchanged. You can keep drafts indefinitely without deploying.
  </Accordion>

  <Accordion title="Can I use the built-in AI tools or Selector Agent while working locally?">
    You need to save your project to Intuned and use these features on the platform by opening the online IDE or Intuned Agent.
  </Accordion>
</AccordionGroup>

## Choose an online or local workflow

| Feature             | Online IDE                                 | Local CLI                                      |
| ------------------- | ------------------------------------------ | ---------------------------------------------- |
| **Setup**           | Browser-based with no local setup          | Requires Python or Node.js and the Intuned CLI |
| **Platform AI**     | Built into the project workflow            | Available after syncing the project to Intuned |
| **Intuned Agent**   | Run from Intuned                           | Run with the external agent plugin             |
| **Version control** | Intuned checkpoints and deployment history | Your Git repository and CI/CD workflow         |
| **IDE choice**      | Intuned's web editor                       | Your preferred local editor                    |

Every project supports both workflows. Sync Intuned changes before saving from a local checkout.

## Related resources

<CardGroup>
  <Card title="Local development CLI" icon="terminal" href="/main/02-features/local-development-cli">
    Develop locally with your own IDE and Git workflow
  </Card>

  <Card title="Intuned Agent" icon="robot" href="/main/02-intuned-agent/overview">
    AI agent that builds and maintains browser automations
  </Card>

  <Card title="AuthSessions" icon="key" href="/main/02-features/auth-sessions">
    Build authenticated automations
  </Card>

  <Card title="How the platform works" icon="book" href="/main/00-getting-started/how-intuned-works">
    Understand Intuned's architecture and execution model
  </Card>
</CardGroup>


# Runs (single executions)
Source: https://intunedhq.com/docs/main/02-features/runs-single-executions

Execute browser automations on demand with immediate, single-purpose API calls

## Overview

Standalone Runs let you execute browser automations on demand. It's the simplest way to run your browser automations.

Use Standalone Runs when you need to trigger a browser automation:

* **In response to user actions** — Submit a form when a user clicks "Apply Now", fetch account details on request, or pull data when a customer connects their account
* **As part of a workflow** — Scrape data for a single item, validate a URL, or extract information from a page as one step in a larger process

<Tip>
  For batch processing or scheduled executions, see [Jobs (batch
  executions)](./jobs-batched-executions).
</Tip>

The Run Playground in the dashboard lets you trigger Runs manually—useful for testing your automations before integrating them into your application.

## How it works

### Project configuration

Your **replication** settings control how many Runs can execute at the same time. `maxCount` sets the most machines your project can scale up to, and `minCount` sets how many stay ready at all times.

<Tabs>
  <Tab title="Online IDE">
    Select **Files** > **Intuned.json**. Update the settings as needed.

    <img alt="Replication settings in the online IDE" />
  </Tab>

  <Tab title="CLI">
    ```jsonc Intuned.json theme={null}
    {
      "replication": {
        "minCount": 1,
        "maxCount": 3,
        // ...
      }
    }
    ```
  </Tab>
</Tabs>

When you deploy your project, Intuned keeps `minCount` machines ready to handle requests. Each Run is picked up by one machine. As Runs come in and the ready machines get busy, the project scales up—adding machines up to `maxCount`. If all machines are busy at the ceiling, additional Runs wait in a queue.

Machines added above your `minCount` baseline are removed when they're no longer needed. You're billed for active compute only—idle machines, including your `minCount` baseline, aren't billed when they're not running anything.

<Note>
  Your workspace has a limit on total deployed machines across all projects.
  Contact support if you need to increase this limit.
</Note>

### Triggering a Standalone Run

<Note>
  Before triggering a Standalone Run, make sure your project is deployed.
</Note>

<Tabs>
  <Tab title="Dashboard (Playground)">
    From the dashboard, go to **Your Project** > **Runs** > **Start Run**. Enter all the inputs for your Run and select "Start Run" to trigger it. Once triggered, you'll be redirected to the Run details page where you can monitor progress and view results.

    <img alt="UI Run Playground" />
  </Tab>

  <Tab title="API">
    Call the [Run start endpoint](/docs/client-apis/api-reference/projectsruns/run-api--start) with the API name and parameters. The response includes a Run ID that you can use to monitor progress and get results. Then use the [Run result endpoint](/docs/client-apis/api-reference/projectsruns/run-api--result) with the Run ID to check status and get the result when complete.

    <CodeGroup>
      ```typescript Typescript theme={null}
      const result = await intuned.projects.runs.start(projectName, {
        api: "<api-name>", // Name of the API to execute
        parameters: {
          // Parameters to pass to the API
        },
        retry: {
          // (Optional) retry configuration
          maximumAttempts: 3, // (Optional) Maximum number of Attempts for the Run. Default is 3
        },
        requestTimeout: 600, // (Optional) Timeout for an Attempt. Default is 600 seconds (10 minutes)
        proxy: "<proxy-url>", // (Optional) Proxy URL, or { url, regex? } — see Proxies docs
        saveTrace: true, // (Optional) Whether to save a browser trace for the Run. Default is true
        authSession: {
          // AuthSession configuration. Required for authenticated Projects
        },
        sink: {
          // (Optional) Sink configuration to send Run results to external systems
        },
      });

      console.log(`Started run ${result.runId}`); // Run ID

      // Poll for results (or use sinks to receive results automatically—see "Get results without polling" below)
      while (true) {
        const runResult = await intuned.projects.runs.result(
          projectName,
          result.runId
        );

        if (runResult.status === "pending") {
          await new Promise((resolve) => setTimeout(resolve, 5000)); // Wait before checking again
          continue;
        }

        console.log(
          `Run ${runResult.runId} finished with status ${runResult.status}`
        );
        // Process results
        break;
      }
      ```

      ```python Python theme={null}
      from intuned_client import models

      started_run = intuned.projects.runs.start(
        project_name=project_name,
        body=models.RunStartRequestBody(
          api="<api_name>", # Name of the API to execute
          parameters={ # Parameters to pass to the API

          },
          retry={ # (Optional) retry configuration
            "maximum_attempts": 3 # (Optional) Maximum number of Attempts for the Run. Default is 3
          },
          request_timeout=600, # (Optional) Timeout for an Attempt. Default is 600 seconds (10 minutes)
          proxy="<proxy_url>", # (Optional) Proxy URL, or { url, regex? } — see Proxies docs
          save_trace=True, # (Optional) Whether to save a browser trace for the Run. Default is true
          auth_session={ # AuthSession configuration. Required for authenticated Projects

          },
          sink={ # (Optional) Sink configuration to send Run results to external systems

          },
        ),
      )

      print(f"Started run {started_run.run_id}") # Run ID

      # Poll for results (or use sinks to receive results automatically—see "Get results without polling" below)
      while True:
        run_result = client.projects.runs.result(
          project_name=project_name,
          run_id=started_run.run_id,
        )

        if run_result.status == "pending":
          time.sleep(5) # Wait before checking again
          continue

        print(f"Run {run_result.run_id} finished with status {run_result.status}")
        # Process results
        break

      ```
    </CodeGroup>

    `proxy` accepts a URL string or an object `{ url, regex? }` for conditional path-based routing. Set `timezone` alongside `proxy`. See [Proxy options](/docs/main/02-features/stealth-mode-captcha-solving-proxies#proxy-options).
  </Tab>

  <Tab title="CLI">
    <Note>
      Run from your project directory (where Intuned Settings is located), or specify the project explicitly with `-p <project-name>`.
    </Note>

    Pass the Run configuration as a JSON string or a path to a JSON file:

    ```bash theme={null}
    intuned platform runs start '{"api": "my-awesome-api", "parameters": {"param1": "value1"}}'
    ```

    With retry and timeout options:

    ```bash theme={null}
    intuned platform runs start '{"api": "my-awesome-api", "parameters": {"param1": "value1"}, "retry": {"maximumAttempts": 3}, "requestTimeout": 600}'
    ```

    Using a file:

    ```bash theme={null}
    intuned platform runs start @run-config.json
    ```

    The command returns a Run ID. Use it to check status and results:

    ```bash theme={null}
    # List recent runs (filter by status, API name, etc.)
    intuned platform runs list --filter status=pending

    # Get details for a specific run
    intuned platform runs get <run-id>
    ```

    See [Runs Start](/docs/main/05-references/cli/platform#runs-start) and [Runs Get](/docs/main/05-references/cli/platform#runs-get) for all options.
  </Tab>
</Tabs>

#### Get results without polling

Intuned supports **webhook** and **S3** sinks to deliver Run results automatically when complete.

<Tabs>
  <Tab title="Dashboard (Playground)">
    While you have the Run Playground open to start a Run, select the **Sink** tab to configure webhook or S3 sinks for result delivery.

    <CodeGroup>
      ```jsonc Webhook theme={null}
      {
        "type": "webhook",
        "url": "<webhook-url>",
        "headers": { // (Optional) Additional headers to include in the webhook request. Useful for authentication
          //...
        },
        "skipOnFail": false // (Optional) Whether to skip sending the webhook if the Run failed. Default is false
      }
      ```

      ```jsonc S3 theme={null}
      {
        "type": "s3",

        // S3-compatible bucket configuration
        "bucket": "<bucket-name>",
        "accessKeyId": "<access-key-id>",
        "secretAccessKey": "<secret-access-key>",
        "region": "<region>",
        "prefix": "<prefix>", // (Optional) Prefix to add to the object key
        "endpoint": "<custom-endpoint>", // (Optional) Custom S3 endpoint for non-AWS providers (e.g., R2)
        "forcePathStyle": false, // (Optional) Whether to force path-style URLs. Enable it for S3-compatible providers that require it (e.g., Supabase Storage)

        "skipOnFail": false // (Optional) Whether to skip sending the webhook if the Run failed. Default is false
      }
      ```
    </CodeGroup>

    <img alt="UI Run Playground Sink Configuration" />
  </Tab>

  <Tab title="API">
    <CodeGroup>
      ```typescript Typescript - webhook theme={null}
      await client.projects.runs.start(
        projectName,
        {
          sink: {
            type: "webhook",
            url: "<webhook-url>",
            headers: { // (Optional) Additional headers to include in the webhook request. Useful for authentication
             //...
            },
            skipOnFail: false, // (Optional) Whether to skip sending the webhook if the Run failed. Default is false
          },
          // ...
        }
      )
      ```

      ```typescript Typescript - S3 theme={null}
      client.projects.runs.start(projectName, {
        sink: {
          type: "s3",

          // S3-compatible bucket configuration
          bucket: "<bucket-name>",
          accessKeyId: "<access-key-id>",
          secretAccessKey: "<secret-access-key>",
          region: "<region>",
          prefix: "<prefix>", // (Optional) Prefix to add to the object key
          endpoint: "<custom-endpoint>", // (Optional) Custom S3 endpoint for non-AWS providers (e.g., R2)
          forcePathStyle: false, // (Optional) Whether to force path-style URLs. Enable it for S3-compatible providers that require it (e.g., Supabase Storage)

          skipOnFail: false, // (Optional) Whether to skip sending the webhook if the Run failed. Default is false
        },
        api: "",
        parameters: {},
      });
      ```

      ```python Python - webhook theme={null}
      from intuned_client import models

      client.projects.runs.start(
        project_name=project_name,
        body=models.RunStartRequestBody(
          api="<api_name>",
          parameters={},
          sink={
            "type": "webhook",
            "url": "<webhook-url>",
            "headers": { # (Optional) Additional headers to include in the webhook request. Useful for authentication
                # ...
            },

            "skip_on_fail": False, # (Optional) Whether to skip sending the webhook if the Run failed. Default is false
          },
          # ...
        ),
      )
      ```

      ```python Python - S3 theme={null}
      from intuned_client import models

      client.projects.runs.start(
        project_name=project_name,
        body=models.RunStartRequestBody(
          api="<api_name>",
          parameters={},
          sink={
            "type": "s3",

            # S3-compatible bucket configuration
            "bucket": "<bucket-name>",
            "access_key_id": "<access-key-id>",
            "secret_access_key": "<secret-access-key>",
            "region": "<region>",
            "prefix": "<prefix>", # (Optional) Prefix to add to the object key
            "endpoint": "<custom-endpoint>", # (Optional) Custom S3 endpoint for non-AWS providers (e.g., R2)
            "force_path_style": False, # (Optional) Whether to force path-style URLs. Enable it for S3-compatible providers that require it (e.g., Supabase Storage)


            "skip_on_fail": False, # (Optional) Whether to skip sending the webhook if the Run failed. Default is false
          },
          # ...
        ),
      )
      ```
    </CodeGroup>
  </Tab>

  <Tab title="CLI">
    <Note>
      Run from your project directory (where Intuned Settings is located), or specify the project explicitly with `-p <project-name>`.
    </Note>

    Include `sink` in the run configuration JSON:

    ```bash theme={null}
    # Webhook sink
    intuned platform runs start '{
      "api": "my-awesome-api",
      "parameters": {},
      "sink": {
        "type": "webhook",
        "url": "<webhook-url>"
      }
    }'
    ```

    ```bash theme={null}
    # S3 sink
    intuned platform runs start '{
      "api": "my-awesome-api",
      "parameters": {},
      "sink": {
        "type": "s3",
        "bucket": "<bucket-name>",
        "accessKeyId": "<access-key-id>",
        "secretAccessKey": "<secret-access-key>",
        "region": "<region>"
      }
    }'
    ```
  </Tab>
</Tabs>

### AuthSession Support

For authenticated Projects, each Run must specify an AuthSession to use. Before each Attempt runs, Intuned automatically validates the AuthSession and can recreate it if needed. This ensures your automation code never runs with an invalid session.

<Note>
  Runs use `authSession` (camelCase) in TypeScript and the REST API. This is
  different from Jobs, which use `auth_session` (snake\_case) in all languages.
  Python SDK uses `auth_session` for both.
</Note>

<Tip>
  Before triggering a Run, you need an AuthSession created. Check out
  [AuthSessions - Create an
  AuthSession](./auth-sessions#4-create-an-authsession).
</Tip>

<Tabs>
  <Tab title="Dashboard (Playground)">
    While you have the Run Playground open to start a Run, select the AuthSession to use in the **Authentication** section and configure it.

    <img alt="UI Run Playground AuthSession" />
  </Tab>

  <Tab title="API">
    Specify the AuthSession to use in the run configuration:

    <CodeGroup>
      ```typescript Typescript theme={null}
      await intuned.projects.runs.start(
        projectName,
        {
          authSession: {
            id: "<auth-session-id>", // Auth session ID to use for the Run
            autoRecreate: true, // (Optional) Whether to automatically recreate the auth session if initial check fails. Default is true
            checkAttempts: 3, // (Optional) Number of attempts to check for session validity. Default is 3
            createAttempts: 3, // (Optional) Number of attempts to create a new session if needed. Default is 3
          },
          // ...
        }
      )
      ```

      ```python Python theme={null}
      from intuned_client import models

      client.projects.runs.start(
        project_name=project_name,
        body=models.RunStartRequestBody(
          api="<api_name>",
          parameters={},
          auth_session={
            "id": "<auth-session-id>", # Auth session ID to use for the Run
            "auto_recreate": True, # (Optional) Whether to automatically recreate the auth session if initial check fails. Default is true
            "check_attempts": 3, # (Optional) Number of attempts to check for session validity. Default is 3
            "create_attempts": 3, # (Optional) Number of attempts to create a new session if needed. Default is 3
          },
          # ...
        ),
      )
      ```
    </CodeGroup>

    <Tip>
      Intuned supports different types of AuthSessions. Check out [AuthSessions -
      AuthSession types](./auth-sessions#authsession-types) for more information.
    </Tip>
  </Tab>

  <Tab title="CLI">
    <Note>
      Run from your project directory (where Intuned Settings is located), or specify the project explicitly with `-p <project-name>`.
    </Note>

    Include `authSession` in the run configuration JSON:

    ```bash theme={null}
    intuned platform runs start '{
      "api": "my-awesome-api",
      "parameters": {},
      "authSession": {
        "id": "<auth-session-id>"
      }
    }'
    ```
  </Tab>
</Tabs>

Each Attempt will produce an **AuthSession Validate** Run before it executes to ensure the session is valid.

### Execution model

When you start a Standalone Run, it executes a single API with the parameters you specify.

Browser automations can sometimes fail due to temporary issues (network problems, element not found, timeouts). To handle this, Runs support automatic retries through multiple Attempts. The Run will try one or more Attempts until it succeeds or runs out of retries.

Each Run produces a **result** or **error** based on the final Attempt's outcome. Each Attempt has its own **result** or **error**, along with detailed logs and a browser trace for debugging.

The Run starts in a `Pending` state, transitions to `Started` when an Attempt begins, and finally to `Success`, `Failed`, or `Canceled` based on the outcome.

For more details about Runs and Attempts, see [Intuned in depth: Runs and Attempts](../01-learn/deep-dives/intuned-indepth#runs-and-attempts).

### Monitoring

Intuned tracks every Run and its Attempts, giving you full visibility into your automation executions. From the dashboard, you can view Run records, inspect individual Attempts, and access detailed logs and browser traces for debugging.

For more details, see [Monitoring and traces](./observability-monitoring-logs).

## Best practices

* Choose an appropriate **maximum concurrent requests** value based on your expected load. While you're not charged for idle compute time, setting a reasonable limit helps manage resource usage effectively.
* Set **request timeouts** to reasonable values to ensure that failed Attempts are detected quickly, allowing for faster retries and reducing overall execution time.
* Use [**sinks** (webhook or S3)](#using-sinks) to receive Run results automatically, reducing the need for polling and improving efficiency.
* If your use case involves batch or scheduled executions, consider transitioning to [**Jobs**](./jobs-batched-executions) for better management and scalability.
* **Set appropriate retry attempts.** Increase retries for websites with flaky behavior or network issues to improve success rates.

## Limitations

* **Workspace machine limit**: Your workspace has a limit on total deployed machines across all projects. If you need more capacity, contact support to increase this limit.
* **Sink delivery is at-least-once**: Results sent to sinks may be delivered multiple times in rare failure scenarios. Make sure your webhook or processing logic handles duplicate deliveries idempotently.

## FAQs

<AccordionGroup>
  <Accordion title="What's the difference between Standalone Runs and Jobs?">
    Standalone Runs execute a single API immediately when you call the endpoint. Jobs orchestrate multiple API executions together with scheduling, retries, and concurrency management. Use Standalone Runs for one-off or user-triggered executions; use Jobs for batch processing and recurring automations.
  </Accordion>

  <Accordion title="How do retries work?">
    Each Run can have multiple Attempts. If an Attempt fails, Intuned
    automatically retries up to your configured `maximumAttempts` (default is 3).
    Each Attempt has its own logs and browser trace for debugging. The Run
    succeeds if any Attempt succeeds, and fails only after all Attempts are
    exhausted.
  </Accordion>

  <Accordion title="Can I cancel a Run in progress?">
    Yes. Use the Run cancel endpoint with the Run ID to cancel a pending or
    running Run. The Run status changes to `Canceled`.
  </Accordion>

  <Accordion title="How long can a Run take?">
    Each Attempt has a configurable `requestTimeout` (default 600 seconds / 10 minutes). If an Attempt exceeds this timeout, it fails and triggers a retry if attempts remain. For long-running automations, increase the timeout or use `extendTimeout` within Jobs.
  </Accordion>
</AccordionGroup>

## Related resources

<CardGroup>
  <Card title="AuthSessions" icon="key" href="./auth-sessions">
    Authenticated browser automations
  </Card>

  <Card title="Run API reference" icon="code" href="/client-apis/api-reference/projectsruns/run-api--start">
    Programmatic access to Standalone Runs
  </Card>

  <Card title="Sinks API reference" icon="webhook" href="/client-apis/api-reference/sinks/overview">
    Webhook and S3 result delivery
  </Card>

  <Card title="Monitoring and traces" icon="chart-line" href="./observability-monitoring-logs">
    Debug and monitor Run executions
  </Card>
</CardGroup>


# Stealth mode, CAPTCHA solving, and proxies
Source: https://intunedhq.com/docs/main/02-features/stealth-mode-captcha-solving-proxies



## Overview

Websites block automated traffic by examining browser fingerprints, detecting automation frameworks, and checking IP reputation. Intuned provides four features to help your automations avoid detection: proxies, headful mode, stealth mode, and CAPTCHA solving.

<Tip>
  **Before you start:** - Stealth mode and CAPTCHA solving are experimental—let
  us know if you encounter issues - Check the site's terms of service and
  `robots.txt` to ensure responsible automation - Stealth mode and CAPTCHA
  solving only work on the Intuned platform, not with local `intuned run`
  commands
</Tip>

## Which features do you need?

Start simple and add features as needed. Each addresses a different blocking mechanism:

| If you're seeing...              | Enable...                    |
| -------------------------------- | ---------------------------- |
| IP-based blocking, rate limiting | Proxies                      |
| Headless browser detection       | Stealth mode or headful mode |
| Fingerprint/automation detection | Stealth mode                 |
| CAPTCHAs blocking your flow      | CAPTCHA solving              |

## Proxies

Proxies route traffic through different IP addresses, making your requests appear to come from different locations. Use them to bypass IP-based blocking and rate limiting.

### Proxy types

| Type            | Description                          | Cost               | Best for                                          |
| --------------- | ------------------------------------ | ------------------ | ------------------------------------------------- |
| **Residential** | IPs from real ISP users              | Per GB (expensive) | Heavily protected sites that block datacenter IPs |
| **Datacenter**  | IPs from cloud data centers          | Per IP/month       | General scraping where IP rotation is sufficient  |
| **ISP**         | Datacenter-hosted but ISP-registered | Per IP/month       | Balance of reliability and cost                   |

<Warning>
  Residential proxies bill per GB of traffic. Monitor usage carefully to avoid
  unexpected charges.
</Warning>

### Auto proxy (Intuned-managed)

If you don't want to buy or manage your own proxy, set the proxy value to `intuned://auto` instead of a provider URL. Intuned resolves this to a working proxy from its managed pool at run time, health-checking candidates automatically.

Set it once as a project default in `intuned.json` (see [`defaults.proxy`](/docs/main/05-references/intuned-json)), or from the CLI with [`intuned dev proxy set`](/docs/main/05-references/cli/dev#proxy-set), which defaults to `intuned://auto` when no URL is given. You can also pass `intuned://auto` directly anywhere a proxy URL is accepted, including the run, AuthSession, and job-level `proxy` parameters below.

<Tip>
  Auto proxy resolves automatically for platform runs and in [Hosted
  projects](/docs/main/02-features/online-ide) (the online IDE), which provide a
  proxy for you. In [Connected
  projects](/docs/main/02-features/local-development-cli) (the local CLI), set the
  `INTUNED_PROXY_URL` environment variable to a fixed proxy—otherwise no
  proxy is applied.
</Tip>

### Proxy options

Anywhere a proxy is accepted for runs, AuthSessions, jobs, Web Tasks, or project defaults, pass either:

* A proxy URL string (for example `"http://username:password@domain:port"` or `"intuned://auto"`)
* An object `{ url, regex? }` for conditional URL-based proxying
* A non-empty array of ordered proxy rule objects

Set `timezone` alongside `proxy` to make the browser clock match the proxy's geography. Use a valid [IANA timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) identifier such as `"America/New_York"`.

The same proxy formats work for job-level `proxy` and [`defaults.proxy`](/docs/main/05-references/intuned-json) in `Intuned.json`. You can also set a timezone from the CLI with [`intuned dev proxy set --timezone`](/docs/main/05-references/cli/dev#proxy-set).

**Run level with timezone:**

<CodeGroup>
  ```typescript theme={null}
  import { IntunedClient } from "@intuned/client";

  const intunedClient = new IntunedClient({
    workspaceId: "123e4567-e89b-12d3-a456-426614174000",
    apiKey: process.env["INTUNED_API_KEY"] ?? "",
  });

  async function run() {
    const result = await intunedClient.projects.runs.start("my-project", {
      parameters: {
        param1: "value1",
        param2: 42,
        param3: true,
      },
      api: "my-awesome-api",
      proxy: {
        url: "http://username:password@domain:port",
      },
      timezone: "America/New_York",
    });

    console.log(result);
  }

  run();
  ```

  ```python theme={null}
  from intuned_client import IntunedClient
  from intuned_client import models
  import os


  with IntunedClient(
      workspace_id="123e4567-e89b-12d3-a456-426614174000",
      api_key=os.getenv("INTUNED_API_KEY", ""),
  ) as ic_client:

      res = ic_client.projects.runs.start(
        project_name="my-project",
        body=models.RunStartRequestBody(
          parameters={
              "param1": "value1",
              "param2": 42,
              "param3": True,
          },
          api="my-awesome-api",
          proxy={
              "url": "http://username:password@domain:port",
          },
          timezone="America/New_York",
        ),
      )

      # Handle response
      print(res)
  ```
</CodeGroup>

#### Conditional URL proxying

Add `regex` to proxy only requests whose URL matches an [RE2](https://github.com/google/re2/wiki/Syntax) pattern. Matching requests use the configured upstream proxy; nonmatching requests connect directly.

```json theme={null}
{
  "proxy": {
    "url": "http://username:password@domain:port",
    "regex": "^https://api\\.example\\.com/"
  }
}
```

Intuned matches against the request URL's scheme, host, and percent-encoded pathname. Query strings and fragments are excluded. For example, `https://ipinfo.io/json?token=secret` is matched as `https://ipinfo.io/json`. Matching is unanchored unless you add `^` or `$`. RE2 does not support lookaround or backreferences.

Conditional routing requires HTTPS inspection. When `regex` is set, Intuned creates an ephemeral certificate authority for the proxy process and configures its managed Chromium browser to trust that certificate. Without `regex`, Intuned uses standard HTTPS tunneling, does not terminate TLS, and does not pass a proxy certificate to the browser. You don't need to install a certificate on the host system.

#### Ordered proxy rules

Use an array when different request URLs need different proxy pools. Intuned checks the rules from top to bottom and uses the first matching rule. If no rule matches, the request connects directly.

```json theme={null}
{
  "proxy": [
    {
      "url": [
        "http://username:password@proxy-1.example.com:8080",
        "http://username:password@proxy-2.example.com:8080"
      ],
      "regex": "^https://api\\.example\\.com/"
    },
    {
      "url": null,
      "regex": "^https://status\\.example\\.com/health$"
    },
    {
      "url": "http://username:password@fallback.example.com:8080"
    }
  ]
}
```

A rule without `regex` matches every remaining request, so later rules are unreachable. Set `url` to `null` to connect matching requests directly and stop evaluation. `intuned://auto` is not supported inside a rule array.

For API retries and AuthSession creation attempts, Intuned randomly selects one URL from each rule's pool. Each pool is selected independently, and the same URL can be selected on consecutive attempts. AuthSession checks stay on their saved selections. Recorder, Web Task, and other non-attempt flows use the first URL in each pool.

### Configure proxies

Configure proxies at three levels. Use any format described in [Proxy options](#proxy-options).

**Run level**—Use the `proxy` parameter when creating a Run:

<CodeGroup>
  ```typescript theme={null}
  import { IntunedClient } from "@intuned/client";

  const intunedClient = new IntunedClient({
    workspaceId: "123e4567-e89b-12d3-a456-426614174000",
    apiKey: process.env["INTUNED_API_KEY"] ?? "",
  });

  async function run() {
    const result = await intunedClient.projects.runs.start("my-project", {
      parameters: {
        param1: "value1",
        param2: 42,
        param3: true,
      },
      api: "my-awesome-api",
      proxy: "http://username:password@domain:port",
    });

    console.log(result);
  }

  run();
  ```

  ```python theme={null}
  from intuned_client import IntunedClient
  from intuned_client import models
  import os


  with IntunedClient(
      workspace_id="123e4567-e89b-12d3-a456-426614174000",
      api_key=os.getenv("INTUNED_API_KEY", ""),
  ) as ic_client:

      res = ic_client.projects.runs.start(
        project_name="my-project",
        body=models.RunStartRequestBody(
          parameters={
              "param1": "value1",
              "param2": 42,
              "param3": True,
          },
          api="my-awesome-api",
          proxy="http://username:password@domain:port",
        ),
      )

      # Handle response
      print(res)
  ```
</CodeGroup>

**AuthSession level**—Use for authenticated sessions that need consistent IPs:

<CodeGroup>
  ```typescript theme={null}
  import { IntunedClient } from "@intuned/client";

  const intunedClient = new IntunedClient({
    workspaceId: "123e4567-e89b-12d3-a456-426614174000",
    apiKey: process.env["INTUNED_API_KEY"] ?? "",
  });

  async function run() {
    const result = await intunedClient.projects.authSessions.create.start(
      "my-project",
      {
        parameters: {
          username: "john.doe",
          password: "password",
        },
        proxy: "http://proxy.example.com:8080",
      }
    );

    console.log(result);
  }

  run();
  ```

  ```python theme={null}
  from intuned_client import IntunedClient
  from intuned_client import models
  import os


  with IntunedClient(
      workspace_id="123e4567-e89b-12d3-a456-426614174000",
      api_key=os.getenv("INTUNED_API_KEY", ""),
  ) as ic_client:

      res = ic_client.projects.auth_sessions.create.start(
        project_name="my-project",
        body=models.AuthSessionsCreateStartRequestBody(
          parameters={
              "username": "john.doe",
              "password": "password",
          },
          id="auth-session-123",
          proxy="http://proxy.example.com:8080",
        ),
      )

      # Handle response
      print(res)
  ```
</CodeGroup>

**Job level**—Use the `proxy` field in job settings. See [Job Settings](/docs/client-apis/api-reference/projectsjobs/create-job).

<Tip>
  Configure proxies at the AuthSession level when you need consistent IP
  addresses to maintain login state.
</Tip>

### Best practices

* **Test without proxies first**—Use datacenter proxies or your own IP before purchasing residential proxies
* **Use residential selectively**—Only for sites that actively block datacenter IPs. Monitor traffic usage due to per-GB billing
* **Rotate proxies**—Distribute requests across multiple proxies to avoid rate limiting
* **Test before committing**—Some proxies are slow or unreliable. Verify performance with your target sites before production use
* **Combine with other features**—Proxies work best alongside stealth mode and headful mode

## Headful mode

Headful mode runs the browser with a visible GUI instead of headless. This produces a more realistic browser fingerprint and avoids headless-specific detection.

### Enable headful mode

Add the `headful` key to your `intuned.json`:

```json theme={null}
{
  "headful": true
}
```

## Stealth mode

Stealth mode runs your automation on Intuned Browser, our Chromium-based stealth browser, and applies additional anti-detection flags at launch. This reduces common automation fingerprints and makes your browser less likely to be flagged as a bot.

### Requirements

* **Playwright v1.55 or higher**—Check [Playwright browsers support](/docs/main/05-references/playwright-browsers-support) for supported versions. We recommend upgrading to the latest Playwright version for the best stealth mode compatibility.
* **Platform only**—Doesn't work with local CLI execution

### Enable stealth mode

```json theme={null}
{
  "stealthMode": {
    "enabled": true
  }
}
```

<Tip>
  If you change stealth settings while a browser session is already open in the
  IDE, restart that browser session so the new browser binary and launch flags
  are applied.
</Tip>

## CAPTCHA solving

When you enable CAPTCHA solving, Intuned automatically detects and solves CAPTCHAs during your automation.

<Note>
  CAPTCHA is a general term for challenges that verify you're human. This
  includes reCAPTCHA, hCaptcha, Cloudflare Turnstile, and similar services.
</Note>

<Info>
  CAPTCHA solving can take time. The duration depends on the CAPTCHA type and
  the difficulty settings configured on the target website. Simple checkbox
  challenges may resolve in seconds, while more complex challenges can take
  longer.
</Info>

### Requirements

* **Headful mode required**—Set `headful` to `true`
* **Stealth mode required**—Set `stealthMode.enabled` to `true`
* **Online IDE or deployed runs**—Doesn't work in the local CLI runtime

### Supported CAPTCHA types

* Google reCAPTCHA v2 and Enterprise
* Cloudflare Turnstile and Cloudflare interstitial challenges
* AWS WAF CAPTCHA
* GeeTest
* FunCaptcha (Arkose Labs)
* Lemin CAPTCHA
* TikTok CAPTCHA (puzzle and rotate challenges)
* Custom image CAPTCHAs (with selectors)
* Text CAPTCHAs (with selectors)

### Configure CAPTCHA solving

<Tabs>
  <Tab title="Enable Cloudflare">
    ```json theme={null}
    {
      "headful": true,
      "stealthMode": {
        "enabled": true
      },
      "captchaSolver": {
        "enabled": true,
        "cloudflare": {
          "enabled": true
        },
        "settings": {
          "autoSolve": true,
          "solveDelay": 2000,
          "maxRetries": 6,
          "timeout": 30000
        }
      }
    }
    ```
  </Tab>

  <Tab title="Specific providers">
    ```json theme={null}
    {
      "headful": true,
      "stealthMode": {
        "enabled": true
      },
      "captchaSolver": {
        "enabled": true,
        "googleRecaptcha": {
          "enabled": true
        },
        "settings": {
          "autoSolve": true,
          "solveDelay": 2000,
          "maxRetries": 3,
          "timeout": 30000
        }
      }
    }
    ```
  </Tab>

  <Tab title="Custom image CAPTCHAs">
    For custom image-based CAPTCHAs, you must provide CSS selectors to identify the CAPTCHA elements:

    ```json theme={null}
    {
      "headful": true,
      "stealthMode": {
        "enabled": true
      },
      "captchaSolver": {
        "enabled": true,
        "customCaptcha": {
          "enabled": true,
          "imageLocators": ["#captcha-image", ".custom-captcha img"],
          "inputLocators": ["#captcha-input", ".captcha-field input"],
          "submitLocators": ["#submit-button", "button[type='submit']"]
        },
        "settings": {
          "autoSolve": true,
          "solveDelay": 2000,
          "maxRetries": 3,
          "timeout": 30000
        }
      }
    }
    ```

    | Option           | Description                              |
    | ---------------- | ---------------------------------------- |
    | `imageLocators`  | CSS selectors for the CAPTCHA image      |
    | `inputLocators`  | CSS selectors for the answer input field |
    | `submitLocators` | CSS selectors for the submit button      |
  </Tab>

  <Tab title="TikTok">
    ```json theme={null}
    {
      "headful": true,
      "stealthMode": {
        "enabled": true
      },
      "captchaSolver": {
        "enabled": true,
        "tiktok": {
          "enabled": true
        },
        "settings": {
          "autoSolve": true,
          "solveDelay": 2000,
          "maxRetries": 10,
          "timeout": 30000
        }
      }
    }
    ```

    TikTok CAPTCHAs appear as either a **puzzle** (slide a piece into place) or a **rotate** (spin an image to the correct angle). Both are solved automatically using the SadCaptcha API.
  </Tab>

  <Tab title="Text CAPTCHAs">
    For text-based challenges (questions or puzzles):

    ```json theme={null}
    {
      "headful": true,
      "stealthMode": {
        "enabled": true
      },
      "captchaSolver": {
        "enabled": true,
        "text": {
          "enabled": true,
          "labelLocators": ["#captcha-question", ".challenge-text"],
          "inputLocators": ["#answer-input", ".captcha-answer"],
          "submitLocators": ["#verify-button", "button.submit"]
        },
        "settings": {
          "autoSolve": true,
          "solveDelay": 2000,
          "maxRetries": 3,
          "timeout": 30000
        }
      }
    }
    ```

    | Option           | Description                              |
    | ---------------- | ---------------------------------------- |
    | `labelLocators`  | CSS selectors for the challenge text     |
    | `inputLocators`  | CSS selectors for the answer input field |
    | `submitLocators` | CSS selectors for the submit button      |
  </Tab>
</Tabs>

### Settings reference

| Setting      | Default | Description                                           |
| ------------ | ------- | ----------------------------------------------------- |
| `autoSolve`  | `true`  | Automatically solve detected CAPTCHAs                 |
| `solveDelay` | `2000`  | Milliseconds to wait before solving (more human-like) |
| `maxRetries` | `10`    | Number of solve attempts before failing               |
| `timeout`    | `30000` | Maximum time (ms) to wait for solve                   |

<Tip>
  Increase `solveDelay` if websites detect bot-like behavior. A small delay
  makes automation appear more human.
</Tip>

### Helper methods

Control CAPTCHA solving with runtime helper methods. Wait for CAPTCHAs to resolve, subscribe to status updates, or pause the solver during automation.

**Quick example:**

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from intuned_runtime.captcha import wait_for_captcha_solve
    from intuned_browser import go_to_url

    await go_to_url(page=page, url="https://example.com")
    await wait_for_captcha_solve(page=page, timeout_s=120.0)

    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import { waitForCaptchaSolve } from '@intuned/runtime';
    import { goToUrl } from '@intuned/browser';

    await goToUrl({ page, url: "https://example.com" });
    await waitForCaptchaSolve(page, { timeoutInMs: 120000 });
    ```
  </Tab>
</Tabs>

For complete documentation including all helper methods, parameters, and usage patterns, see the CAPTCHA helpers reference for [Python](/docs/main/05-references/runtime-sdk-python/captcha-helpers) or [TypeScript](/docs/main/05-references/runtime-sdk-typescript/captcha-helpers).

<Tip>
  If navigation or a button click may trigger the challenge, prefer the
  wrapper-style helpers so Intuned can wait for the page to settle before it
  checks solve status.
</Tip>

### Important considerations

* **Selector accuracy**—For custom and text CAPTCHAs, test selectors in browser console before deploying
* **Timeout settings**—Complex CAPTCHAs need higher timeouts
* **Service limits**—Some sites limit CAPTCHA attempts. Monitor solve rates to avoid additional blocks

<Tip>
  Use the [selector agent](/docs/main/02-features/online-ide#selector-agent) to
  generate reliable selectors for custom and text CAPTCHA elements.
</Tip>

## Best practices

* **Start simple**: Enable features incrementally. First test your automation without protection, then add features as needed.
* **Keep dependencies updated**: Upgrade to the latest Playwright version. Stealth mode works best with newer versions, as updates include improved fingerprint spoofing and compatibility with the latest detection methods.
* **Use residential proxies sparingly**: Due to traffic-based billing, reserve residential proxies for websites that definitely block datacenter proxies.
* **Combine with other strategies**: These features work best alongside other automation best practices like rate limiting and respectful request patterns.

## Limitations

### Stealth mode

* Platform only—doesn't work with local CLI
* Requires Playwright v1.55+—upgrade to the latest version for best results
* New detection methods may emerge that stealth mode doesn't handle yet—report your case and we're happy to support it

### CAPTCHA solving

* Platform only—doesn't work with local CLI
* Must complete within configured timeout
* Some CAPTCHA types aren't supported—contact us if you encounter one

### Proxies

* Residential proxies have traffic-based billing
* Some sites block known proxy IP ranges
* Proxy speed varies by provider

## FAQs

<AccordionGroup>
  <Accordion title="Do I need all features together?">
    No. Each addresses different blocking mechanisms. Start with the simplest configuration and add features as needed. For most protected sites, headful + stealth mode is a good starting point.
  </Accordion>

  <Accordion title="Why don't stealth mode and CAPTCHA solving work locally (Intuned CLI)?">
    These features require the full Intuned platform environment to properly
    modify browser behavior. Use Intuned IDE to develop and test stealth-related
    features.
  </Accordion>

  <Accordion title="How much does stealth mode cost?">
    Stealth mode is included in Intuned platform plans at no additional cost. See [Plans and billing](/docs/main/05-references/plans-and-billing) for details.
  </Accordion>

  <Accordion title="Why is my automation still getting blocked even with stealth mode enabled?">
    Websites vary widely in what they check. Some:

    * Use novel CAPTCHA types not yet supported
    * Check for multiple factors beyond fingerprint and IP
    * Have aggressive rate limiting
    * Require additional headers or request patterns

    Try combining stealth mode with proxies and headful mode. If you're still blocked, you might need to adjust your request patterns, add delays, or use a different proxy provider. We're continuously improving stealth mode to address new detection methods. Let us know if you encounter issues.
  </Accordion>

  <Accordion title="Does stealth mode slow down automation?">
    No significant impact. Startup time may increase slightly, but runtime
    performance is comparable to standard Playwright.
  </Accordion>

  <Accordion title="Can I use stealth mode with headless browsers?">
    Yes.
  </Accordion>

  <Accordion title="Should I use residential or datacenter proxies?">
    Start with datacenter proxies—they're cheaper. Only upgrade to residential if your target site actively blocks datacenter IPs. Monitor residential usage carefully due to per-GB billing.
  </Accordion>
</AccordionGroup>

## Related resources

<CardGroup>
  <Card title="CAPTCHA Helpers (Python)" icon="python" href="/main/05-references/runtime-sdk-python/captcha-helpers">
    Complete runtime SDK reference for CAPTCHA helper methods in Python
  </Card>

  <Card title="CAPTCHA Helpers (TypeScript)" icon="js" href="/main/05-references/runtime-sdk-typescript/captcha-helpers">
    Complete runtime SDK reference for CAPTCHA helper methods in TypeScript
  </Card>

  <Card title="Intuned.json reference" icon="file-code" href="/main/05-references/intuned-json">
    Full configuration options for headful, stealthMode, and captchaSolver
  </Card>

  <Card title="CLI reference" icon="terminal" href="/main/05-references/cli/overview">
    CLI documentation including the --proxy flag
  </Card>

  <Card title="Playwright browsers support" icon="browser" href="/main/05-references/playwright-browsers-support">
    Playwright version requirements
  </Card>

  <Card title="Plans and billing" icon="credit-card" href="/main/05-references/plans-and-billing">
    Feature availability by plan
  </Card>
</CardGroup>


# Usage and billing
Source: https://intunedhq.com/docs/main/02-features/usage-pricing-monitoring-alerting

View consumption metrics, set limits, and manage invoices

## Overview

Intuned charges a fixed monthly plan cost plus pay-as-you-go for usage beyond your plan limits. Billing happens on the 1st of each month.

Two pages help you monitor and control spending:

* **[Usage](https://app.intuned.io/usage)** — Track consumption metrics
* **[Billing](https://app.intuned.io/billing)** — Manage payments, limits, and invoices

## Check your usage

Go to [app.intuned.io/usage](https://app.intuned.io/usage) to see your consumption for the current billing period.

<img alt="Usage Page" />

The page shows:

* **Usage breakdown** — Each metric with current consumption (in usage units, not dollars)
* **Daily usage chart** — Compare two metrics over time to spot patterns or spikes
* **Project breakdown** — Which projects are consuming the most resources

<Note>
  To see dollar amounts and overage costs, go to the [Billing page](https://app.intuned.io/billing).
</Note>

### IntunedAgent usage

The **IntunedAgent** project tracks usage from Intuned Agent tasks (creating, updating, or fixing projects). This includes AI spend for generated code, compute hours for running task APIs, and any captcha solves during web interactions.

## Set usage limits

By default, Intuned doesn't block usage when you exceed plan limits—it charges overages. To avoid unexpected costs, set hard limits on specific metrics.

1. Go to the [Billing page](https://app.intuned.io/billing).
2. In the usage table, find the metric and select **<Icon icon="gear" />**.
3. Enter your limit value and select **Save Changes**.

<img alt="Setting Usage Limit" />

When you hit a limit, further consumption of that resource is blocked until the next billing cycle.

### Configure alerts

Get notified before hitting limits:

1. In the usage table, find the metric and select **<Icon icon="bell" />**.
2. Enter the threshold value for notifications.
3. Select **Save Changes**.

<Tip>
  Change the workspace alert email in the usage table to forward alerts to a different address.
</Tip>

## Change your plan

Go to the [Billing page](https://app.intuned.io/billing) to upgrade or downgrade.

**Upgrades** take effect immediately. Your invoice includes a prorated charge for the new plan, a credit for unused time on your current plan, and any overages up to the upgrade date.

**Downgrades** take effect at the start of the next billing cycle. Any overages from the current cycle are billed normally.

<Note>
  To cancel your subscription, downgrade to the Free plan.
</Note>

For Enterprise plans with custom limits and pricing, [contact our team](https://cal.com/team/intuned/get-started).

See the [Plans reference](/docs/main/05-references/plans-and-billing) for plan details and pricing.

## View invoices and manage payment

From the [Billing page](https://app.intuned.io/billing), you can:

* View expected charges for the next billing cycle (including projected overages)
* Download past invoices
* Add or update payment methods

Unpaid invoices display a warning with the amount due.

## Free plan

The Free plan lets you explore Intuned without payment information. It includes limited resources with hard limits—no overages. Once you hit limits, upgrade to continue.

Eligible accounts receive a one-time \$5 AI credit at signup to explore Agent and SDK features. Eligibility is determined during account creation to prevent abuse. Credits are granted automatically—no action required. If your account is not eligible, upgrade your plan or [contact support](mailto:support@intunedhq.com) to use AI features.

<img alt="Free Plan Limits" />

## Usage metrics reference

| Metric                     | What it measures                                                                                                                                                                                                                |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Compute hours              | Time your machine workers spend executing APIs and JobRuns                                                                                                                                                                      |
| AI spend                   | Dollars spent on AI processing from AI SDK methods and Intuned Agent tasks billed through Intuned, charged at provider cost plus a flat 20% platform fee. Requests sent with your own provider key don't count toward AI spend. |
| JobRuns                    | Number of JobRuns executed                                                                                                                                                                                                      |
| Captcha solves             | Captcha challenges solved ([details](/docs/main/02-features/stealth-mode-captcha-solving-proxies))                                                                                                                                   |
| Large machines surcharge   | Additional hours for large machine types                                                                                                                                                                                        |
| X-Large machines surcharge | Additional hours for extra-large machine types                                                                                                                                                                                  |
| Pages processed            | PDF pages processed through Intuned's PDF capabilities                                                                                                                                                                          |

<Warning>
  Large and X-Large machine surcharges can lead to significant overage charges on Developer and Startup plans.
</Warning>

## Workspace limits

Your plan determines workspace constraints beyond usage metrics:

* Maximum concurrent machines
* Maximum machines per project
* Data retention period
* Access to large/x-large machine types

View your current limits on the [Billing page](https://app.intuned.io/billing).

## FAQs

<AccordionGroup>
  <Accordion title="When does my billing cycle start?">
    Billing cycles start on the 1st of every month. Your plan fee is charged along with any overages from the previous month.
  </Accordion>

  <Accordion title="How are overages calculated?">
    Overages are usage beyond your plan limits, charged at per-unit rates. See the [Plans reference](/docs/main/05-references/plans-and-billing) for rates.
  </Accordion>

  <Accordion title="Do unused credits roll over?">
    No. Unused plan allowances don't roll over and aren't refundable.
  </Accordion>

  <Accordion title="What payment methods do you accept?">
    Visit the [Billing page](https://app.intuned.io/billing) and select **Manage Payment Methods** to see options for your region.
  </Accordion>
</AccordionGroup>

## Related resources

<CardGroup>
  <Card title="Plans reference" icon="table" href="/main/05-references/plans-and-billing">
    Plan features and pricing details
  </Card>

  <Card title="Manage your workspace" icon="user" href="/main/03-how-to/manage/manage-workspace">
    Configure workspace settings
  </Card>
</CardGroup>


# External agent
Source: https://intunedhq.com/docs/main/02-intuned-agent/external-agent



Run the Intuned agent locally in your own coding agent instead of the hosted agent. The Intuned Agent Plugin gives it the same skills and browser tooling the hosted agent uses, so you can build, edit, test, and debug automations from your own terminal and IDE.

Supported in Claude Code and Codex.

<Note>
  The external agent is currently in **private preview** and available on
  request. If you'd like access, [contact our
  team](/docs/main/06-resources/help-and-support) — we'll grant access and help you
  set up the plugin.
</Note>

## What you get

Everything the [Intuned Agent](/docs/main/02-intuned-agent/overview) can do — creating, editing, testing, and debugging automations — but running in your own environment instead of the hosted session:

* Works on the same project files as [local development](/docs/main/02-features/local-development-cli), so version control, your IDE, and your own tools stay in the loop.
* Runs and tests automations locally, with a local browser you can watch, or as end-to-end platform test jobs.
* Debugs failing runs where the code lives, including Playwright trace debugging.
* Deploys with the CLI when the project is ready.

## Related resources

<CardGroup>
  <Card title="Intuned Agent overview" icon="robot" href="/main/02-intuned-agent/overview">
    The hosted agent and what it can do
  </Card>

  <Card title="Local development" icon="terminal" href="/main/02-features/local-development-cli">
    Build projects with the local CLI
  </Card>

  <Card title="Using the agent" icon="book" href="/main/02-intuned-agent/using-the-agent">
    Workflows and prompt examples
  </Card>

  <Card title="CLI reference" icon="terminal" href="/main/05-references/cli/overview">
    Complete CLI command documentation
  </Card>
</CardGroup>


# Intuned Agent overview
Source: https://intunedhq.com/docs/main/02-intuned-agent/overview



## What is Intuned Agent?

Intuned Agent is an AI agent that builds, edits, and maintains browser automation Projects. It runs inside the Intuned platform with a real browser, full access to the platform via the [Intuned CLI](/docs/main/05-references/cli/overview), and works in the background as long as needed.

## Where to find the agent

Access the agent at [app.intuned.io/agent](https://app.intuned.io/agent). Chat with the agent, review its plan, approve changes, and deploy — all from one session.

<Frame>
  <video />
</Frame>

## What the agent can do

<AccordionGroup>
  <Accordion title="Create scrapers, RPAs, crawlers, and AI-powered projects" icon="hammer">
    * Scrapers for any website — public, login-protected, bot-protected,
      CAPTCHA-gated - RPA automations — form submissions, data entry, account
      actions - Crawlers — multi-page discovery, sitemaps, content indexing -
      AI-powered automations — [Stagehand](/docs/main/04-integrations/stagehand) /
      [Browser-Use](/docs/main/04-integrations/browser-use)
  </Accordion>

  <Accordion title="Handle AuthSessions, CAPTCHAs, bot detection, and complex pages" icon="shield-halved">
    * Pagination, infinite scroll, load more, detail pages, nested lists,
      iframes - [AuthSessions](/docs/main/02-features/auth-sessions) — secure
      credential collection, auth session setup - [Stealth, CAPTCHA, and
      proxies](/docs/main/02-features/stealth-mode-captcha-solving-proxies) - Network
      interception — prefer XHR/GraphQL over DOM scraping
  </Accordion>

  <Accordion title="Fix broken automations — selectors, timeouts, and failed runs" icon="wrench">
    * Fix from a failed [Run](/docs/main/02-features/runs-single-executions) — broken
      selectors, changed page structure, timeouts, wrong output - Fix from a
      failed [JobRun](/docs/main/02-features/jobs-batched-executions) — investigate
      issues across batched executions - Debug with Playwright trace analysis
  </Accordion>

  <Accordion title="Detect, fix, and redeploy automatically with self-healing projects" icon="rotate">
    Enable self-healing on any project and the agent monitors runs, detects
    issues, writes fixes, and redeploys — without manual intervention. See
    [Self-healing projects](/docs/main/02-intuned-agent/self-healing-projects).
  </Accordion>

  <Accordion title="Deep platform integration" icon="plug">
    The agent isn't an external wrapper — it reads Run and JobRun results,
    traces, and logs, configures
    [Jobs](/docs/main/02-features/jobs-batched-executions), and tests on the same
    infrastructure your Project uses in production.
  </Accordion>
</AccordionGroup>

## Agent in the online IDE

The same agent is also available in the [Intuned online IDE](/docs/main/02-features/online-ide) Chat tab. Use it to make quick edits to an existing Project directly in the code editor, without opening a full agent session.

<Frame>
  <video />
</Frame>

## Cost and billing

The agent consumes AI credits based on tokens used. Free plans include credits to get started. Track spend on the [Usage page](https://app.intuned.io/usage) under the **IntunedAgent** project.

See [Plans and billing](/docs/main/05-references/plans-and-billing) for plan limits and [Usage and billing](/docs/main/02-features/usage-pricing-monitoring-alerting) for tracking spend.

## FAQ

<AccordionGroup>
  <Accordion title="Can I review the code before deploying?">
    Yes. Review and iterate on the plan with **Suggest Changes**, or select **Approve Plan**. After the agent builds, inspect the generated code in the **Code** panel and test results in the **Artifacts** panel. When ready, select **Merge Branch** — choose **Merge & Deploy** to deploy immediately, or **Merge only** to merge without deploying.
  </Accordion>

  <Accordion title="Can the agent bypass CAPTCHAs or bot detection?">
    Yes. The agent enables [stealth mode and CAPTCHA
    solving](/docs/main/02-features/stealth-mode-captcha-solving-proxies) based on what
    it encounters — stealth mode for bot detection, CAPTCHA solving if a challenge
    is still visible after stealth, and proxies for IP-based blocking.
  </Accordion>

  <Accordion title="Can the agent work with authenticated websites?">
    Yes. The agent securely collects credentials (never visible to the AI) and
    sets up [AuthSession](/docs/main/02-features/auth-sessions) login flow APIs.
  </Accordion>

  <Accordion title="Can the agent suggest a schema?">
    Yes. The agent auto-discovers the schema by exploring the site and analyzing
    the data available on the page. It includes the proposed schema in the plan
    for your review.
  </Accordion>

  <Accordion title="Why does the agent take so long?">
    The agent explores and understands the site, generates and tests code, then
    runs a full validation. Complex automations with pagination, edge cases, and
    large data sets take longer.
  </Accordion>

  <Accordion title="Can I close my browser while the agent is working?">
    Yes. The agent continues working in the background. Close your browser or
    navigate away — come back anytime to check progress or review results.
  </Accordion>

  <Accordion title="How is this different from Director.ai?">
    Both tools use AI to generate browser automation code, but they differ in execution:

    |                     | Intuned Agent                                          | Director                                 |
    | ------------------- | ------------------------------------------------------ | ---------------------------------------- |
    | **Execution**       | Deterministic code by default, AI-driven code optional | AI agent controls the browser at runtime |
    | **Data extraction** | Code-based selectors — predictable, fast               | LLM-powered — flexible, slower           |
    | **Output**          | Complete Playwright project                            | Stagehand scripts                        |
    | **Platform**        | Deploys to Intuned with jobs, scheduling, and auth     | -                                        |

    Intuned Agent produces deterministic code by default. It can also generate AI-driven automations using [Stagehand](/docs/main/04-integrations/stagehand) or [Browser-Use](/docs/main/04-integrations/browser-use) when requested.
  </Accordion>

  <Accordion title="Does the agent support CLAUDE.md and custom skills?">
    Not yet, but coming soon. The agent will load your project's CLAUDE.md and
    custom skills to understand your project's structure, conventions, and
    extended capabilities.
  </Accordion>

  <Accordion title="Does the agent work on a branch?">
    Yes. Each session creates an auto-generated branch prefixed with `agent/`
    (e.g. `agent/fix-amazon-scraper-timeout`). Your changes are isolated until you
    merge. View branches from the project's overview page — both new and existing
    projects.
  </Accordion>

  <Accordion title="Can I run multiple sessions at once?">
    Yes. You can run concurrent agent sessions, subject to your workspace's
    concurrency quota. See [Plans and
    billing](/docs/main/05-references/plans-and-billing) for limits.
  </Accordion>

  <Accordion title="What languages does the agent support?">
    TypeScript and Python. The language is chosen when creating a new project.
    When editing an existing project, the agent matches the language of the
    existing code.
  </Accordion>

  <Accordion title="Can the agent install dependencies?">
    Yes. The agent manages `package.json` (TypeScript) or `pyproject.toml`
    (Python) and can add or remove packages as needed.
  </Accordion>

  <Accordion title="Can the agent manage environment variables?">
    Yes. The agent uses a secure tool to request environment variables and secrets
    — it never asks you to paste sensitive values into the chat.
  </Accordion>

  <Accordion title="Can I use my existing Claude Code or Codex subscription?">
    Not directly. The hosted agent uses its own AI credits system, separate from
    any Claude Code, Codex, or provider console subscription. To use your own AI
    instead, run the agent locally with the [External
    agent](/docs/main/02-intuned-agent/external-agent) plugin, which runs inside your
    own Claude Code or Codex and uses that subscription (or API key), consuming
    no Intuned AI credits.
  </Accordion>

  <Accordion title="Can the agent run locally?">
    Yes. The [External agent](/docs/main/02-intuned-agent/external-agent) plugin (private preview, available on request) runs the Intuned agent locally in Claude Code or Codex from your own development environment.
  </Accordion>
</AccordionGroup>

## Guides

<CardGroup>
  <Card title="Using the agent" icon="book" href="/main/02-intuned-agent/using-the-agent">
    Workflows, prompt examples, and capability deep dives
  </Card>

  <Card title="Self-healing projects" icon="rotate" href="/main/02-intuned-agent/self-healing-projects">
    Automated fix, merge, and deploy pipelines
  </Card>

  <Card title="Quickstart: Scraper" icon="database" href="/main/00-getting-started/quickstarts/intuned-agent">
    Build a scraper with the agent step by step
  </Card>

  <Card title="Quickstart: RPA" icon="robot" href="/main/00-getting-started/quickstarts/build-an-rpa-with-intuned-agent">
    Build an RPA automation with the agent
  </Card>
</CardGroup>


# Self-healing projects
Source: https://intunedhq.com/docs/main/02-intuned-agent/self-healing-projects



<Note>Self-healing projects are an experimental feature. We welcome [feedback](/docs/main/06-resources/help-and-support).</Note>

## What are self-healing projects?

Enable self-healing on any project to close the loop between monitoring and fixing. Intuned watches your runs, detects when something breaks, and the agent investigates, writes a fix, and can merge and deploy it — all without manual intervention. The level of autonomy is fully configurable — you control how much the agent does on its own. See [autonomy levels](#autonomy-levels).

## How to enable it

1. Go to your project's overview page.
2. Select **Edit Project**.
3. Under **Self-healing project**, toggle the autonomy levels you want.

<Frame>
  <img alt="Self-healing project settings in Edit Project" />
</Frame>

## How it works

<Steps>
  <Step title="A run completes">
    After each job run or standalone run, Intuned captures metrics — success rate, failure count, run count, and result size — and checks them against anomaly detection rules and historical data.
  </Step>

  <Step title="Anomalies are detected and triaged">
    If a rule triggers, an anomaly is created. The agent investigates each anomaly — comparing healthy and failed runs, reading traces and logs — and decides whether it's a real issue or a false positive.
  </Step>

  <Step title="Issues are surfaced">
    After the agent validates that anomalies point to real problems — by analyzing run results, past data, and traces — confirmed issues with the same root cause are grouped and surfaced in the [Issues panel](#issues-panel).
  </Step>

  <Step title="Agent fixes the issue">
    If **Auto-fix** is enabled, the agent opens a session on a new branch to diagnose and fix the issue.
  </Step>

  <Step title="Fix is merged and deployed">
    If **Auto-merge fixes** and **Auto-deploy fixes** are enabled, the agent merges the fix and deploys the project automatically.
  </Step>
</Steps>

<Note>It may take up to 6 hours for issues to surface. This batching window reduces AI credit costs. The window is configurable, and we're working on making detection more real-time. The exact detection logic is an internal detail and may change as we improve accuracy — we welcome [feedback](/docs/main/06-resources/help-and-support).</Note>

### Anomaly detection

Intuned monitors metrics after each run to detect changes that could indicate a problem — a website updates its layout, response sizes shift, data patterns deviate from recent runs. When an anomaly is detected, the agent doesn't surface it immediately. It investigates first — comparing healthy and failed runs, analyzing traces and logs — and only promotes it to a real issue if it confirms something is broken. False positives are dismissed automatically.

## Autonomy levels

Self-healing projects have four toggles, each adding a layer of autonomy. Enable only the ones you're comfortable with — each level includes everything above it.

We recommend starting with **Advanced monitoring** on all projects to surface issues early. Advanced monitoring and higher autonomy levels consume additional AI credits. See [Plans and billing](/docs/main/05-references/plans-and-billing) for details.

| Toggle                  | What it does                                                             | Instructions                                                |
| ----------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------- |
| **Advanced monitoring** | Automatically detect and surface issues from runs                        | —                                                           |
| **Auto-fix**            | Automatically open an agent session to fix raised issues on a new branch | Describe when sessions should be created automatically      |
| **Auto-merge fixes**    | Automatically merge fix branches                                         | Describe the conditions required before branches are merged |
| **Auto-deploy fixes**   | Automatically deploy the project after a fix branch is merged            | Describe when deployments should happen automatically       |

Auto-fix, auto-merge, and auto-deploy each accept optional **instructions** — free-text guidance the agent follows when performing that action.

<AccordionGroup>
  <Accordion title="Default auto-fix instructions">
    Automatically create remediation sessions for OPEN issues and keep the branch tightly scoped to the surfaced problem.
  </Accordion>

  <Accordion title="Default auto-merge instructions">
    Auto-merge only complete remediation branches that have a clear work summary and no waiting-for-user state.
  </Accordion>

  <Accordion title="Default auto-deploy instructions">
    Auto-deploy only after an auto-merge succeeds; stop if deployment logs surface a failure or if human approval is still required.
  </Accordion>
</AccordionGroup>

## Issues panel

<Frame>
  <img alt="The Issues panel on the project overview page" />
</Frame>

1. **Issues panel** — Open issues appear on the right side of the project overview. Each issue shows a title, timestamp, and a description of the root cause. Dismiss false positives or select an issue for details.
2. **Agent session** — If Auto-fix is enabled, the agent automatically opens a session to fix the issue. The session appears in the Agent Sessions list linked to the issue.
3. **Attach an issue to a message** — Reference an issue directly in the agent input to start a conversation about it. The agent loads the issue context and begins investigating.

## Related resources

<CardGroup>
  <Card title="Using the agent" icon="book" href="/main/02-intuned-agent/using-the-agent">
    Workflows, prompt examples, and the agent interface
  </Card>

  <Card title="Intuned Agent overview" icon="robot" href="/main/02-intuned-agent/overview">
    What the agent can do and where to find it
  </Card>

  <Card title="Jobs" icon="clock" href="/main/02-features/jobs-batched-executions">
    Scheduled and batched executions the agent monitors
  </Card>

  <Card title="Monitoring and traces" icon="chart-line" href="/main/02-features/observability-monitoring-logs">
    Run traces, logs, and observability
  </Card>
</CardGroup>


# Using the agent
Source: https://intunedhq.com/docs/main/02-intuned-agent/using-the-agent



## Creating a new project

Start a new conversation at [app.intuned.io/agent](https://app.intuned.io/agent) to build a project from scratch.

<Frame>
  <video />
</Frame>

<Tabs>
  <Tab title="1. Describe" icon="comments">
    Tell the agent what to build — include the target URL and the data you want
    or the action to automate. Use a **template** to pre-fill your prompt, or a
    **recipe** to add capabilities.
  </Tab>

  <Tab title="2. Plan" icon="list-check">
    The agent explores the site and may ask clarifying questions (usually
    multiple choice) before presenting a plan. Review the proposed schema, APIs,
    and approach — then **Approve** or **Suggest Changes**.
  </Tab>

  <Tab title="3. Build" icon="robot">
    The agent writes code, installs dependencies, and runs tests against the
    live site. This typically takes 30–60 minutes. You can close your browser —
    the agent continues in the background.
  </Tab>

  <Tab title="4. Deploy" icon="rocket">
    Review the generated code in the **Code** panel and test results in the
    **Artifacts** panel. Select **Merge Branch** to deploy — choose **Merge &
    Deploy** or **Merge only**. You can also send a follow-up message to request
    further changes before merging.
  </Tab>
</Tabs>

### Prompt examples

<AccordionGroup>
  <Accordion title="Scraper" icon="magnifying-glass">
    Create a scraper for [https://www.energy.ca.gov/funding-opportunities/solicitations](https://www.energy.ca.gov/funding-opportunities/solicitations)

    For each solicitation, extract:

    * title
    * solicitation number
    * status
    * posted date
    * closing date
    * description

    Download every attachment and include the files in the output.
  </Accordion>

  <Accordion title="RPA / automation" icon="diagram-project">
    Create an RPA project for [https://sandbox.intuned.dev/consultations-auth/book](https://sandbox.intuned.dev/consultations-auth/book)

    This is an authenticated website. Set up AuthSessions so the automation runs with authenticated browser state.

    Create these APIs:

    1. book-consultation — book a new consultation
    2. check-consultation-status — check the status of an existing consultation
    3. confirm-consultation — confirm a pending consultation
    4. get-my-consultations — list all my consultations (add API parameters so the caller can filter by status or date range)
  </Accordion>

  <Accordion title="Crawler" icon="diagram-subtask">
    Create a crawler that can crawl any documentation site and extract structured page content.

    Test it on these sites:

    * [https://intunedhq.com/docs](https://intunedhq.com/docs)
    * [https://react.dev](https://react.dev)
    * [https://docs.anthropic.com](https://docs.anthropic.com)

    For each page, extract:

    * Page title
    * Section headings
    * Body content (as markdown)
    * Links to child pages

    Crawl internal links up to a configurable depth.
  </Accordion>

  <Accordion title="AI-powered automation" icon="sparkles">
    Create an AI-powered job board crawler.

    The crawler should:

    1. Start from a job board listing page, extract all job links, and crawl each one
    2. For known boards like Lever, extract structured data with deterministic selectors (title, location, department, team, commitment, workplace type, description, apply URL)
    3. For other boards (Greenhouse, custom), fall back to AI extraction using the extract data helpers from the @intuned/browser package (e.g. extractData) with the same schema
    4. Deduplicate URLs across payloads using persistentStore
    5. Support configurable max\_depth and max\_pages
  </Accordion>
</AccordionGroup>

## Editing an existing project

Open the project you want to edit from the [projects page](https://app.intuned.io/projects), then start a new agent conversation from the project overview.

<Frame>
  <video />
</Frame>

<Tabs>
  <Tab title="1. Open the project" icon="folder-open">
    Open the project you want to edit from the [projects
    page](https://app.intuned.io/projects), then start a new agent conversation
    from the project overview.
  </Tab>

  <Tab title="2. Describe the change" icon="comments">
    Describe what you want to change — the agent figures out the details. Use a
    **template** for common edits, or a **recipe** to add capabilities.
  </Tab>

  <Tab title="3. Plan" icon="list-check">
    For larger changes, the agent presents a plan for your review. It may ask
    clarifying questions (usually multiple choice). **Approve** or **Suggest
    Changes**. For smaller changes, the agent starts building directly.
  </Tab>

  <Tab title="4. Build" icon="robot">
    The agent implements the changes, runs tests, and validates the results
    against the live site. You can close your browser — the agent continues in
    the background.
  </Tab>

  <Tab title="5. Merge" icon="code-merge">
    Review the changes and select **Merge**. You'll see an editable summary
    before confirming. Choose **Merge** to apply, or **Merge and deploy** to
    apply and deploy in one step.
  </Tab>
</Tabs>

### Prompt examples

<AccordionGroup>
  <Accordion title="Update API result" icon="list-check">
    API name: get-solicitations
    What should change in the result:

    * Add a `categories` array where each item has name and URL
    * Change `price` to an object with `amount` (number) and `currency` (string)

    Test on all the API parameters.
  </Accordion>

  <Accordion title="Fix wrong data output" icon="magnifying-glass">
    API name: get-products
    Field or entity with wrong data:

    * `price` is returning the discounted price instead of the original price

    Current output:

    * price: 29.99

    Expected output:

    * price: 49.99

    Related run IDs:

    * run\_abc123
  </Accordion>

  <Accordion title="Fix timeout error" icon="timer">
    Check last failed runs to find the runs failing with timeout error and fix the issue.
  </Accordion>

  <Accordion title="QA this project" icon="clipboard-check">
    Run all APIs with their default test parameters and verify the outputs are correct. Flag any failures, incorrect data, or missing fields.
  </Accordion>
</AccordionGroup>

## Fix with AI

Navigate to a failed run in the dashboard and select **Fix with AI**.

<Frame>
  <video />
</Frame>

<Tabs>
  <Tab title="1. Find the failed run" icon="folder-open">
    Open your project from the [projects page](https://app.intuned.io/projects)
    and go to the **Runs** tab. Select a run with an error.
  </Tab>

  <Tab title="2. Start a fix" icon="wrench">
    Select **Fix with AI** from the failed run. The agent opens a new
    conversation with the run context already loaded and starts diagnosing the
    issue.
  </Tab>

  <Tab title="3. Build" icon="robot">
    The agent diagnoses the issue using trace analysis and run logs, then fixes
    the code and validates the result. You can close your browser — the agent
    continues in the background.
  </Tab>

  <Tab title="4. Merge" icon="code-merge">
    Review the fix and select **Merge**. You'll see an editable summary before
    confirming. Choose **Merge** to apply, or **Merge and deploy** to apply and
    deploy in one step.
  </Tab>
</Tabs>

### What it can fix

* **Failed runs** — broken selectors, changed page structure, missing elements
* **Wrong data** — incorrect extraction logic, wrong fields, missing data
* **Timeouts** — slow steps, unnecessary waits, performance bottlenecks

## Intuned Agent interface

The agent runs in a multi-panel interface. The main conversation is on the left, with tabbed panels on the right for code, browser, and artifacts.

<Frame>
  <img />
</Frame>

1. **Home** — Return to Intuned home.
2. **New conversation** — Start a new agent session — select a model, pick a project, and choose templates or recipes.
3. **Conversation list** — All your conversations grouped by project, with status indicators and timestamps.
4. **Work summary** — When the agent makes meaningful changes to the project, it adds a summary to the branch. This summary becomes the checkpoint message when you merge.
5. **Merge branch** — Merge the agent's changes into your project. Shows an editable summary before confirming — choose **Merge** to apply, or **Merge and deploy** to apply and deploy in one step. Same action available in the header.
6. **View code** — View the complete project code with diff highlighting. Shows additions and deletions so you can review exactly what the agent changed.
7. **Message input** — Send a new message to the agent. You can queue messages while the agent is working — queued messages are sent automatically after the current turn finishes and the agent stops.
8. **Model selector** — Change which Claude model the agent uses: **Sonnet** (fast and intelligent), **Opus** (most intelligent), or **Haiku** (fastest). Takes effect when you send a message.
9. **Permission mode** — Control how much autonomy the agent has:
   * **With human approval** (default) — asks for input before taking actions.
   * **Autonomous** — acts without approval, ideal for background tasks.
10. **Code panel** — View the project file tree with all changes the agent has made. Diffs are highlighted inline.
11. **Browser panel** — A live view of the browser the agent controls. Watch navigation, clicks, form fills, and data extraction in real time.
12. **Artifacts panel** — While the agent is running, it runs APIs and triggers test jobs. Check the results here — including parameters, responses, errors, and Playwright traces.
13. **Open in IDE** — Opens the branch in the online IDE for direct code editing.
14. **Merge code** — Same merge action as #5, accessible from the header for quick access.
15. **Branch name** — The name of the agent's working branch for this conversation.
16. **Project link** — Link to the project this conversation is associated with.

## Troubleshooting

| Issue                                | What to do                                                                                                                                                                     |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Agent seems stuck during testing** | Platform test jobs can take a while. Check the **Artifacts panel** every few minutes for updated results — the agent polls for completion and continues once the job finishes. |
| **Session timed out**                | Agent sessions have an 8-hour maximum. If your task hits this limit, type "continue" and the agent resumes where it left off.                                                  |

For other issues, [contact Intuned support](/docs/main/06-resources/help-and-support).

## Related resources

<CardGroup>
  <Card title="Intuned Agent overview" icon="robot" href="/main/02-intuned-agent/overview">
    What the agent can do and where to find it
  </Card>

  <Card title="Self-healing projects" icon="rotate" href="/main/02-intuned-agent/self-healing-projects">
    Automated detection, fixing, and redeployment
  </Card>

  <Card title="External agent" icon="terminal" href="/main/02-intuned-agent/external-agent">
    Run the agent locally in your own coding agent
  </Card>

  <Card title="Runs" icon="play" href="/main/02-features/runs-single-executions">
    Single API executions and how to debug them
  </Card>

  <Card title="Jobs" icon="clock" href="/main/02-features/jobs-batched-executions">
    Scheduled and batched executions
  </Card>

  <Card title="AuthSessions" icon="key" href="/main/02-features/auth-sessions">
    Authenticated browser automations
  </Card>

  <Card title="Stealth, CAPTCHA, and proxies" icon="shield-halved" href="/main/02-features/stealth-mode-captcha-solving-proxies">
    Bot detection, CAPTCHA solving, and IP rotation
  </Card>
</CardGroup>


# How to manage API keys
Source: https://intunedhq.com/docs/main/03-how-to/manage/manage-api-keys



Use API keys to authenticate requests to Intuned APIs. Manage your API keys from the [API keys page](https://app.intuned.io/settings/api-keys) in the Intuned dashboard. Check our [API documentation](/docs/client-apis/api-reference/) for details on using API keys.

## Default API key

When you create an Intuned account, Intuned automatically generates a default API key. You cannot delete the default API key, but you can rotate it to generate a new key value. The previous key value stops working immediately after rotation.

## Additional API keys

Create additional API keys for different environments, applications, or team members. You can:

* **Create** new API keys with custom names. You can only view the key value once. If you lose it, you need to create a new key.
* **Rename** existing API keys
* **Revoke** keys that are no longer needed. The key stops working immediately. Any requests using this key will fail.


# How to manage projects
Source: https://intunedhq.com/docs/main/03-how-to/manage/manage-projects



## What is a project?

A Project is both your automation code (browser automation APIs written in Python/TypeScript using Playwright) and your runtime environment where all execution data lives (Run records, Jobs, browser traces, logs, and auth sessions).

Find all your projects on the [Projects page](https://app.intuned.io/projects) in the dashboard.

## Managing projects

### Creating a new project

Create a new Python or TypeScript project from the [Projects page](https://app.intuned.io/projects) or the [Home page](https://app.intuned.io). Start in the [online IDE](/docs/main/02-features/online-ide) or set up locally with the [CLI](/docs/main/02-features/local-development-cli).

### Renaming a project

Change your project's name using the **Edit Project** button in the header of your project page.

<Warning>
  Renaming a project may affect your integrations since the project name is part
  of the URL used to trigger Runs via API.
</Warning>

### Duplicating a project

Duplicate a project directly from the projects table. The duplicated project's name will include " - copy" and a timestamp to differentiate it from the original.

### Undeploying a project

Undeploy a project directly from the projects table.

<Warning>
  Undeploying a project will pause all active Jobs and API access to this
  project will be restricted.
</Warning>

### Deleting a project

Delete a project directly from the projects table. Deleting a project is irreversible and removes all associated data including Runs, Jobs, browser traces, logs, and AuthSessions.

## Deployed vs undeployed projects

**Deployed projects** can be consumed via [Runs](/docs/main/02-features/runs-single-executions) or [Jobs](/docs/main/02-features/jobs-batched-executions). You can trigger your automation APIs, schedule Jobs, and view execution history from the project page.

<img alt="Deployed project example" />

**Undeployed projects** cannot be consumed. You can edit your code and prepare it for deployment, but you won't be able to trigger Runs or Jobs until the project is deployed.

<img alt="Undeployed project example" />

## Online and local development

[Local development](/docs/main/02-features/local-development-cli) uses the CLI so you can iterate on your machine and sync changes to Intuned. The [online IDE](/docs/main/02-features/online-ide) lets you edit and manage the same project directly in the browser. Every project supports both workflows.

<img alt="Local development example" />


# How to manage users
Source: https://intunedhq.com/docs/main/03-how-to/manage/manage-users



## Overview

Invite members, remove them, or view details in your workspace.

## Access user management

Go to **Settings** in the sidebar, then select the **Members** tab.

Or go directly to [Settings > Members](https://app.intuned.io/settings/members).

## Manage users

### Invite users

Select **Invite Members** and enter the email address of the person you want to invite.

### Remove users

Select **Remove** next to the user's name in the Members table. Removed users remain logged in for a few minutes before the change takes effect.


# How to manage your workspace
Source: https://intunedhq.com/docs/main/03-how-to/manage/manage-workspace



## What is a workspace?

A workspace is your team's shared environment in Intuned for collaborating on browser automation projects. Each team should have its own workspace to keep projects and resources organized.

<Note>
  Intuned generates a unique workspace ID, which is required to call API endpoints. This ID cannot be changed. Find your workspace ID in [Workspace Settings](https://app.intuned.io/settings/workspace).
</Note>

## Create a workspace

When you sign up for Intuned, a workspace is automatically created for you.

### Create additional workspaces

In some cases, you may need an additional workspace—for example, if you're part of multiple teams or need different teams to access separate sets of automations. To create additional workspaces, [contact support](mailto:support@intunedhq.com).

## Rename a workspace

You can rename your workspace by going to the [Workspace Settings](https://app.intuned.io/settings/workspace) page and updating the workspace name.

## Delete a workspace

You can delete a specific workspace by contacting [support](mailto:support@intunedhq.com).

<Warning>
  Deleting a workspace is a permanent action and cannot be undone. All projects and resources associated with the workspace will be permanently deleted.
</Warning>

## Manage billing

To manage billing and view usage for your workspace, check out the [Billing](https://app.intuned.io/billing) and [Usage](https://app.intuned.io/usage) pages in the Intuned app.
This includes:

* Viewing and updating your plan
* Viewing invoices
* Managing payment methods
* Opening usage details

You can learn more about billing and usage [here](/docs/main/02-features/usage-pricing-monitoring-alerting).

## Switch between workspaces

You can switch between multiple workspaces using the workspace switcher in the Intuned app. This allows you to easily manage and access different workspaces without needing to log out and log back in.

<img alt="Workspace Switcher" />

## Related resources

<CardGroup>
  <Card title="Manage your workspace team members" icon="users" href="/main/03-how-to/manage/manage-users">
    Invite, remove, and manage members of your workspace
  </Card>

  <Card title="Projects" icon="folder" href="/main/03-how-to/manage/manage-projects">
    Organize your browser automations into projects
  </Card>

  <Card title="API Keys" icon="key" href="/main/03-how-to/manage/manage-api-keys">
    Create and manage API keys for secure access to your workspace resources
  </Card>
</CardGroup>


# How to debug automations
Source: https://intunedhq.com/docs/main/03-how-to/solve/debug-automations



## Overview

When automations fail, the error message alone is sometimes not enough to diagnose the issue. This guide walks you through a systematic debugging workflow: checking error messages, viewing traces, identifying root causes, and implementing fixes.

## Debugging workflow

### 1. Check the error message

Start by accessing the Run details page and checking the error message in the **Error** tab. While error messages provide initial clues, you'll need to view the trace to understand the full context.

<img alt="Checking error message in Error tab" />

### 2. View the Playwright trace

View the trace for the last failed Attempt to get complete visibility into what actually happened during execution. The trace shows:

* Step-by-step screenshots of the automation
* Network requests and responses
* Console logs and errors
* DOM snapshots at each step
* Timing information for each action

To view traces:

1. In the Run details page, scroll down to the **Attempts Timeline**
2. Select the failed attempt to expand it
3. Select **View Trace** to open Playwright's trace viewer

<img alt="Viewing Playwright trace from attempts timeline" />

The trace viewer helps you see exactly where the automation failed and what the page looked like at that moment.

### 3. Identify the root cause

After watching the trace, identify the root cause to guide your fix:

**Website is down or broken** — If the target website shows server errors or is unreachable, wait for it to recover. Consider adding retry logic for temporary outages.

**Page structure changed** — If selectors that previously worked now fail, the website's HTML structure likely changed. Update your selectors to match the new structure.

**Incorrect or outdated selectors** — If the automation times out waiting for an element or gets "strict mode violation" errors:

* The selector may be wrong, too specific, or match multiple elements
* The element may load dynamically (add explicit waits)
* The element may be inside an iframe
* Make selectors more precise or use filtering methods

**Bot detection** — If the trace shows CAPTCHAs, access denied messages, or unexpected behavior, the website detected your automation. See [how to deal with bot detection](/docs/main/02-features/stealth-mode-captcha-solving-proxies).

**Navigation or timing issues** — Pages may not fully load before interactions. Add explicit waits for specific elements or network idle states.

**Element state issues** — Elements exist but aren't visible or enabled for interaction. Use Playwright's auto-waiting or check element states before interacting.

**Viewport differences** — Enforce a specific viewport size in your code to ensure consistent behavior across environments.

The trace also helps identify performance bottlenecks by showing which actions took the longest time and where the automation was blocked.

### 4. Download and review logs

For additional debugging context, download the logs for the Run. Logs contain:

* Console output from your automation code
* Custom log messages you added with `console.log()` or logging libraries
* System-level information about the execution environment

To download logs:

1. In the Run details page, scroll down to the **Attempts Timeline**
2. Select the attempt to expand it
3. Select **View Logs**

<img alt="Viewing logs from attempts timeline" />

Logs are especially helpful for understanding the flow of your automation logic and identifying where specific issues occur. Learn more about [observability, monitoring, and logs](/docs/main/02-features/observability-monitoring-logs).

### 5. Reproduce with Run in IDE

After identifying the issue, try to reproduce it by running the failed Run in the IDE:

1. In the Run details page, select the **Play** icon to launch **Run in IDE**
2. The IDE opens with the Run's parameters pre-loaded and starts executing
3. Watch the execution to confirm you can reproduce the issue

<img alt="Running failed Run in IDE" />

<Note>
  Sometimes issues aren't reproducible in the IDE because of differences between the IDE and deployed environments:

  * Different viewport sizes
  * Bot detection in headless mode
  * Different execution timing or network conditions
  * Browser extensions or settings
  * Geographic location or IP-based restrictions

  If you encounter issues that only occur in production, review the trace and logs from the deployed Run to identify environment-specific factors.
</Note>

### 6. Implement the fix and deploy

Once you've identified and confirmed the issue:

1. **Implement your fix** — Update selectors, add wait conditions, handle edge cases, or make other necessary changes
2. **Test your changes** — Run the automation in the Intuned IDE or locally to verify the fix works
3. **Deploy** — Deploy your changes. Changes won't take effect until you deploy

### 7. QA the fix

After deploying, verify the fix works by:

* **Re-running the failed API** — Call the API again with the same parameters that previously failed
* **Re-triggering the Job** — If the failure occurred in a Job, trigger the Job again to ensure it completes successfully

For comprehensive QA strategies, see [how to QA automation results](/docs/main/03-how-to/solve/qa-automation-results).


# How to make automations faster
Source: https://intunedhq.com/docs/main/03-how-to/solve/make-automations-faster



Slow automations increase compute costs and reduce throughput. This guide covers techniques to speed up your browser automations.

<Tip>
  Use the [trace viewer](/docs/main/02-features/observability-monitoring-logs#inspect-run-attempts) to identify which actions are taking the most time before optimizing.
</Tip>

## Block unnecessary resources

Block heavy assets like images, stylesheets, and fonts that aren't needed for your automation. This reduces page load time significantly.

<Accordion title="Example">
  <CodeGroup>
    ```typescript TypeScript theme={null}
    import { BrowserContext, Page } from "playwright";

    const BLOCKED_RESOURCE_TYPES = [
      'image',
      'stylesheet', 
      'font',
      'media',
      'texttrack',
      'beacon',
      'csp_report',
      'imageset'
    ];

    export default async function automation(
      params: any,
      page: Page,
      context: BrowserContext
    ) {
      await page.route('**/*', (route) => {
        const request = route.request();
        
        if (BLOCKED_RESOURCE_TYPES.includes(request.resourceType())) {
          return route.abort();
        }
        
        return route.continue();
      });

      await page.goto('https://example.com');
      
      return {};
    }
    ```

    ```python Python theme={null}
    from playwright.async_api import Page
    from typing import Any, Dict

    BLOCKED_RESOURCE_TYPES = [
        'image',
        'stylesheet',
        'font',
        'media',
        'texttrack',
        'beacon',
        'csp_report',
        'imageset'
    ]


    async def automation(
        page: Page,
        params: Dict[str, Any] | None = None,
        **kwargs
    ):
        async def handle_route(route):
            request = route.request
            
            if request.resource_type in BLOCKED_RESOURCE_TYPES:
                await route.abort()
            else:
                await route.continue_()
        
        await page.route('**/*', handle_route)
        await page.goto('https://example.com')
        
        return {}
    ```
  </CodeGroup>
</Accordion>

## Use network interception instead of DOM scraping

Intercept API responses directly instead of parsing the DOM. This is often 10x faster and more reliable. Use browser dev tools to find which API endpoints return the data you need.

The following examples show the same scraper—first using DOM manipulation, then using network interception.

<Accordion title="DOM Manipulation (Slower)">
  <CodeGroup>
    ```typescript TypeScript theme={null}
    import { BrowserContext, Page } from "playwright";
    import { goToUrl } from "@intuned/browser";

    export default async function automation(
      params: any,
      page: Page,
      context: BrowserContext
    ) {
      await goToUrl({ page, url: "https://www.ycombinator.com/companies" });

      // Wait for companies to load
      await page.getByText("Loading companies...").waitFor({ state: "hidden" });
      await page.locator('a[href^="/companies/"]').first().waitFor();

      // Get all company card links
      const companyCards = page.locator('a[href^="/companies/"]:not([href="/companies"])');
      const count = await companyCards.count();

      const companies: { name: string; industry: string; tags: string[] }[] = [];

      for (let i = 0; i < count; i++) {
        const card = companyCards.nth(i);
        const href = await card.getAttribute("href");
        if (!href || href === "/companies") continue;

        const tagLinks = card.locator('a[href^="/companies?"]');
        const tagCount = await tagLinks.count();

        const tags: string[] = [];
        let industry = "";

        for (let j = 0; j < tagCount; j++) {
          const tagText = (await tagLinks.nth(j).innerText()).trim();
          const tagHref = await tagLinks.nth(j).getAttribute("href");

          if (tagHref?.includes("industry=") && !industry) {
            industry = tagText;
          }
          tags.push(tagText);
        }

        const slug = href.replace("/companies/", "");
        companies.push({ name: slug.replace(/-/g, " "), industry, tags });
      }

      return companies;
    }
    ```

    ```python Python theme={null}
    from playwright.async_api import Page
    from typing import Any, Dict
    from intuned_browser import go_to_url


    async def automation(
        page: Page,
        params: Dict[str, Any] | None = None,
        **kwargs
    ):
        await go_to_url(page=page, url="https://www.ycombinator.com/companies")

        # Wait for companies to load
        await page.get_by_text("Loading companies...").wait_for(state="hidden")
        await page.locator('a[href^="/companies/"]').first.wait_for()

        # Get all company card links
        company_cards = page.locator('a[href^="/companies/"]:not([href="/companies"])')
        count = await company_cards.count()

        companies = []

        for i in range(count):
            card = company_cards.nth(i)
            href = await card.get_attribute("href")
            if not href or href == "/companies":
                continue

            tag_links = card.locator('a[href^="/companies?"]')
            tag_count = await tag_links.count()

            tags = []
            industry = ""

            for j in range(tag_count):
                tag_text = (await tag_links.nth(j).inner_text()).strip()
                tag_href = await tag_links.nth(j).get_attribute("href")

                if tag_href and "industry=" in tag_href and not industry:
                    industry = tag_text
                tags.append(tag_text)

            slug = href.replace("/companies/", "")
            companies.append({"name": slug.replace("-", " "), "industry": industry, "tags": tags})

        return companies
    ```
  </CodeGroup>
</Accordion>

<Accordion title="Network Interception (Faster)">
  <CodeGroup>
    ```typescript TypeScript theme={null}
    import { BrowserContext, Page } from "playwright";
    import { goToUrl } from "@intuned/browser";

    export default async function automation(
      params: any,
      page: Page,
      context: BrowserContext
    ) {
      const responsePromise = page.waitForResponse(async (response) => {
        const requestUrl = response.request().url();
        if (requestUrl.includes("indexes/*/queries")) {
          const jsonResponse = JSON.parse((await response.body()).toString());
          if (jsonResponse.results[0].hits.length > 1) {
            return true;
          }
        }
        return false;
      });

      await goToUrl({ page, url: "https://www.ycombinator.com/companies" });

      const response = await responsePromise;
      const jsonResponse = JSON.parse((await response.body()).toString());
      
      const companies = jsonResponse.results[0].hits.map((hit: any) => ({
        name: hit.name,
        website: hit.website,
        industry: hit.industry,
        tags: hit.tags,
      }));

      return companies;
    }
    ```

    ```python Python theme={null}
    from playwright.async_api import Page
    from typing import Any, Dict
    from intuned_browser import go_to_url
    import json


    async def automation(
        page: Page,
        params: Dict[str, Any] | None = None,
        **kwargs
    ):
        async def match_response(response):
            if "indexes/*/queries" in response.request.url:
                body = await response.body()
                json_response = json.loads(body.decode())
                if len(json_response["results"][0]["hits"]) > 1:
                    return True
            return False

        response_promise = page.wait_for_response(match_response)

        await go_to_url(page=page, url="https://www.ycombinator.com/companies")

        response = await response_promise
        body = await response.body()
        json_response = json.loads(body.decode())

        companies = [
            {
                "name": hit["name"],
                "website": hit["website"],
                "industry": hit["industry"],
                "tags": hit["tags"],
            }
            for hit in json_response["results"][0]["hits"]
        ]

        return companies
    ```
  </CodeGroup>
</Accordion>

## Set shorter timeouts

The default Playwright timeout is 30 seconds. If your pages load quickly, reduce it to fail faster on errors.

<Accordion title="Example">
  <CodeGroup>
    ```typescript TypeScript theme={null}
    import { BrowserContext, Page } from "playwright";

    export default async function automation(
      params: any,
      page: Page,
      context: BrowserContext
    ) {
      // Set a shorter default timeout (in milliseconds)
      page.setDefaultTimeout(10000); // 10 seconds instead of 30
      
      await page.goto('https://example.com');
      await page.locator('#fast-loading-element').click();
      
      return {};
    }
    ```

    ```python Python theme={null}
    from playwright.async_api import Page
    from typing import Any, Dict


    async def automation(
        page: Page,
        params: Dict[str, Any] | None = None,
        **kwargs
    ):
        # Set a shorter default timeout (in milliseconds)
        page.set_default_timeout(10000)  # 10 seconds instead of 30
        
        await page.goto('https://example.com')
        await page.locator('#fast-loading-element').click()
        
        return {}
    ```
  </CodeGroup>
</Accordion>

## Optimize waiting strategies

### Avoid `waitForTimeout`

Avoid `waitForTimeout()` with arbitrary delays—they waste time when pages load fast and fail when pages load slowly. Instead, wait for something specific.

```typescript theme={null}
// Wait for the table to appear
await page.locator('#data-table').waitForElementState("visible");

// Wait for at least one row to exist
await page.locator('.table-row').first().waitForElementState("visible");
```

### Wait for DOM or network to settle

After actions that trigger page changes, use Intuned helpers:

* [waitForDomSettled](/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/waitForDomSettled) — After clicking buttons that modify the page
* [withNetworkSettledWait](/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/withNetworkSettledWait) — After navigation or actions that trigger API calls

### Extract lists efficiently

When scraping lists, avoid iterating with locators—each locator call auto-waits, adding milliseconds per row that compounds over hundreds of elements.

Instead:

1. Wait for the list container to be visible
2. Extract all data in a single `evaluate()` call using `querySelectorAll`

<Accordion title="Example: Scraping a list">
  <CodeGroup>
    ```typescript TypeScript theme={null}
    import { BrowserContext, Page } from "playwright";

    export default async function automation(
      params: any,
      page: Page,
      context: BrowserContext
    ) {
      await page.goto('https://example.com/products');

      // Wait for the list container to be visible
      await page.locator('.product-list').waitFor({ state: 'visible' });

      // Extract all data in a single evaluate call
      const products = await page.evaluate(() => {
        return Array.from(document.querySelectorAll('.product-item')).map(el => ({
          name: el.querySelector('.name')?.textContent?.trim(),
          price: el.querySelector('.price')?.textContent?.trim(),
        }));
      });

      return { products };
    }
    ```

    ```python Python theme={null}
    from playwright.async_api import Page
    from typing import Any, Dict


    async def automation(
        page: Page,
        params: Dict[str, Any] | None = None,
        **kwargs
    ):
        await page.goto('https://example.com/products')

        # Wait for the list container to be visible
        await page.locator('.product-list').wait_for(state='visible')

        # Extract all data in a single evaluate call
        products = await page.evaluate("""
            () => Array.from(document.querySelectorAll('.product-item')).map(el => ({
                name: el.querySelector('.name')?.textContent?.trim(),
                price: el.querySelector('.price')?.textContent?.trim(),
            }))
        """)

        return {"products": products}
    ```
  </CodeGroup>
</Accordion>

## Batch evaluate calls

Each `evaluate()` call is a round trip to the browser. Combine multiple queries into a single call.

<Accordion title="Example">
  <CodeGroup>
    ```typescript TypeScript theme={null}
    import { BrowserContext, Page } from "playwright";

    export default async function automation(
      params: any,
      page: Page,
      context: BrowserContext
    ) {
      await page.goto('https://example.com');
      
      // ❌ Bad: Multiple round trips
      // const title = await page.evaluate(() => document.title);
      // const url = await page.evaluate(() => window.location.href);
      // const count = await page.evaluate(() => document.querySelectorAll('.item').length);
      
      // ✅ Good: Single batched call
      const data = await page.evaluate(() => {
        return {
          title: document.title,
          url: window.location.href,
          count: document.querySelectorAll('.item').length,
          items: Array.from(document.querySelectorAll('.item')).map(el => ({
            text: el.textContent?.trim()
          }))
        };
      });
      
      return data;
    }
    ```

    ```python Python theme={null}
    from playwright.async_api import Page
    from typing import Any, Dict


    async def automation(
        page: Page,
        params: Dict[str, Any] | None = None,
        **kwargs
    ):
        await page.goto('https://example.com')
        
        # ❌ Bad: Multiple round trips
        # title = await page.evaluate('() => document.title')
        # url = await page.evaluate('() => window.location.href')
        # count = await page.evaluate('() => document.querySelectorAll(".item").length')
        
        # ✅ Good: Single batched call
        data = await page.evaluate("""
            () => ({
                title: document.title,
                url: window.location.href,
                count: document.querySelectorAll('.item').length,
                items: Array.from(document.querySelectorAll('.item')).map(el => ({
                    text: el.textContent?.trim()
                }))
            })
        """)
        
        return data
    ```
  </CodeGroup>
</Accordion>

## Defer heavy processing

Move expensive operations outside the browser context. Extract raw data first, then process it after the automation completes. This includes image processing, LLM calls, data transformation, and file conversions.

<Accordion title="Example">
  <CodeGroup>
    ```typescript TypeScript theme={null}
    import { BrowserContext, Page } from "playwright";

    export default async function automation(
      params: any,
      page: Page,
      context: BrowserContext
    ) {
      await page.goto('https://example.com/products');
      
      // Extract raw data only - process later
      const rawData = await page.evaluate(() => {
        return Array.from(document.querySelectorAll('.product')).map(el => ({
          imageUrl: el.querySelector('img')?.src,
          title: el.querySelector('.title')?.textContent,
          price: el.querySelector('.price')?.textContent
        }));
      });
      
      return { rawData };
    }
    ```

    ```python Python theme={null}
    from playwright.async_api import Page
    from typing import Any, Dict


    async def automation(
        page: Page,
        params: Dict[str, Any] | None = None,
        **kwargs
    ):
        await page.goto('https://example.com/products')
        
        # Extract raw data only - process later
        raw_data = await page.evaluate("""
            () => Array.from(document.querySelectorAll('.product')).map(el => ({
                imageUrl: el.querySelector('img')?.src,
                title: el.querySelector('.title')?.textContent,
                price: el.querySelector('.price')?.textContent
            }))
        """)
        
        return {"rawData": raw_data}
    ```
  </CodeGroup>
</Accordion>

## Use AuthSessions

Instead of logging in every run, use [AuthSessions](/docs/main/02-features/auth-sessions) to reuse authenticated browser state. This can save 5-30 seconds per run depending on the login complexity.

## Replace AI code with deterministic code

AI agents require multiple LLM calls per action. If your automation does predictable, repeatable steps, replace AI code with direct selectors. Use AI only for unpredictable page structures or as a fallback when deterministic code fails.

<Accordion title="Stagehand Agent (Slower)">
  <CodeGroup>
    ```typescript TypeScript theme={null}
    import z from "zod";
    import { Stagehand } from "@browserbasehq/stagehand";
    import type { BrowserContext, Page } from "playwright";
    import { attemptStore, getAiGatewayConfig } from "@intuned/runtime";

    interface Params {
      query: string;
    }

    async function getWebSocketUrl(cdpUrl: string): Promise<string> {
      if (cdpUrl.includes("ws://") || cdpUrl.includes("wss://")) {
        return cdpUrl;
      }

      const versionUrl = cdpUrl.endsWith("/")
        ? `${cdpUrl}json/version`
        : `${cdpUrl}/json/version`;
      const response = await fetch(versionUrl);
      const data = await response.json();
      return data.webSocketDebuggerUrl;
    }

    export default async function automation(
      { query }: Params,
      page: Page,
      _context: BrowserContext
    ) {
      const { baseUrl, apiKey } = await getAiGatewayConfig();
      const cdpUrl = attemptStore.get("cdpUrl") as string;
      const webSocketUrl = await getWebSocketUrl(cdpUrl);

      const stagehand = new Stagehand({
        env: "LOCAL",
        localBrowserLaunchOptions: {
          cdpUrl: webSocketUrl,
          viewport: { width: 1280, height: 800 },
        },
        model: {
          modelName: "openai/gpt-5-mini",
          apiKey,
          baseURL: baseUrl,
        },
      });
      await stagehand.init();

      try {
        await page.goto("https://example.com/products");

        // Each Stagehand action still requires LLM calls.
        await stagehand.act(`Search for "${query}" and select the first result.`);

        const productSchema = z.object({
          product: z
            .object({
              name: z.string(),
              price: z.string(),
            })
            .nullable(),
        });

        return await stagehand.extract(
          "Extract the product name and price.",
          productSchema
        );
      } finally {
        await stagehand.close();
      }
    }
    ```

    ```python Python theme={null}
    from typing import TypedDict

    from intuned_runtime import attempt_store, get_ai_gateway_config
    from playwright.async_api import Page
    from pydantic import BaseModel
    from stagehand import AsyncStagehand
    from stagehand.types.model_config_param import ModelConfigParam
    from stagehand.types.session_start_params import Browser, BrowserLaunchOptions


    class Params(TypedDict):
        query: str


    class Product(BaseModel):
        name: str
        price: str


    class ProductResponse(BaseModel):
        product: Product | None


    async def automation(page: Page, params: Params, **_kwargs):
        query = params["query"]
        base_url, api_key = get_ai_gateway_config()
        cdp_url = attempt_store.get("cdp_url")

        model_name = "openai/gpt-5-mini"
        model_config: ModelConfigParam = {
            "model_name": model_name,
            "api_key": api_key,
            "base_url": base_url,
            "provider": "openai",
        }

        client = AsyncStagehand(
            server="local",
            model_api_key=api_key,
            local_ready_timeout_s=30.0,
        )
        launch_options: BrowserLaunchOptions = {"headless": False}
        if cdp_url is not None:
            launch_options["cdp_url"] = str(cdp_url)

        browser: Browser = {"type": "local", "launch_options": launch_options}
        session = await client.sessions.start(model_name=model_name, browser=browser)
        session_id = session.data.session_id

        try:
            await page.goto("https://example.com/products")

            # Each Stagehand action still requires LLM calls.
            await client.sessions.act(
                id=session_id,
                input=f'Search for "{query}" and select the first result.',
                options={"model": model_config},
            )

            result = await client.sessions.extract(
                id=session_id,
                instruction="Extract the product name and price.",
                options={"model": model_config},
                schema={
                    "type": "object",
                    "properties": {
                        "product": {
                            "type": ["object", "null"],
                            "properties": {
                                "name": {"type": "string"},
                                "price": {"type": "string"},
                            },
                            "required": ["name", "price"],
                        }
                    },
                    "required": ["product"],
                },
            )

            return ProductResponse.model_validate(result.data.result)
        finally:
            await client.sessions.end(session_id)
    ```
  </CodeGroup>
</Accordion>

<Accordion title="Deterministic Code (Faster)">
  <CodeGroup>
    ```typescript TypeScript theme={null}
    import { BrowserContext, Page } from "playwright";

    interface Params {
      query: string;
    }

    export default async function automation(
      { query }: Params,
      page: Page,
      context: BrowserContext
    ) {
      await page.goto('https://example.com/products');
      
      // Direct selectors - instant execution
      await page.fill('#search-input', query);
      await page.click('button[type="submit"]');
      await page.locator('.product').first().click();
      
      // Direct extraction - no LLM needed
      const product = await page.evaluate(() => {
        return {
          name: document.querySelector('.product-name')?.textContent,
          price: document.querySelector('.product-price')?.textContent
        };
      });
      
      return { product };
    }
    ```

    ```python Python theme={null}
    from playwright.async_api import Page
    from typing import Any, Dict


    async def automation(
        page: Page,
        params: Dict[str, Any] | None = None,
        **kwargs
    ):
        query = params.get("query", "")
        
        await page.goto('https://example.com/products')
        
        # Direct selectors - instant execution
        await page.fill('#search-input', query)
        await page.click('button[type="submit"]')
        await page.locator('.product').first.click()
        
        # Direct extraction - no LLM needed
        product = await page.evaluate("""
            () => ({
                name: document.querySelector('.product-name')?.textContent,
                price: document.querySelector('.product-price')?.textContent
            })
        """)
        
        return {"product": product}
    ```
  </CodeGroup>
</Accordion>

## Use fetch for static content

For static content, fetch HTML directly instead of using the browser. Only use the browser when JavaScript rendering is required.

<Accordion title="Example">
  <CodeGroup>
    ```typescript TypeScript theme={null}
    import { BrowserContext, Page } from "playwright";

    export default async function automation(
      params: any,
      page: Page,
      context: BrowserContext
    ) {
      const url = 'https://example.com/page';
      
      // Try fetch first
      const response = await fetch(url);
      const html = await response.text();
      
      // Check if data exists in static HTML
      if (html.includes('data-product-id')) {
        const matches = html.matchAll(/data-product-id="(\d+)"/g);
        const products = Array.from(matches).map(m => ({ id: m[1] }));
        return { products };
      }
      
      // Fallback to browser if JavaScript rendering is needed
      await page.goto(url);
      const products = await page.evaluate(() => {
        return Array.from(document.querySelectorAll('.product')).map(el => ({
          id: el.getAttribute('data-product-id'),
          name: el.textContent
        }));
      });
      
      return { products };
    }
    ```

    ```python Python theme={null}
    from playwright.async_api import Page
    from typing import Any, Dict
    import httpx
    import re


    async def automation(
        page: Page,
        params: Dict[str, Any] | None = None,
        **kwargs
    ):
        url = 'https://example.com/page'
        
        # Try fetch first
        async with httpx.AsyncClient() as client:
            response = await client.get(url)
            html = response.text
        
        # Check if data exists in static HTML
        if 'data-product-id' in html:
            matches = re.findall(r'data-product-id="(\d+)"', html)
            products = [{"id": match} for match in matches]
            return {"products": products}
        
        # Fallback to browser if JavaScript rendering is needed
        await page.goto(url)
        products = await page.evaluate("""
            () => Array.from(document.querySelectorAll('.product')).map(el => ({
                id: el.getAttribute('data-product-id'),
                name: el.textContent
            }))
        """)
        
        return {"products": products}
    ```
  </CodeGroup>
</Accordion>

## Adjust browser configuration

If optimizations don't help and simple actions are still slow, the site may be JavaScript-heavy. Consider these configuration changes:

* **Use a larger machine** — Upgrade your machine size in [replication settings](/docs/main/05-references/intuned-json#replication) for resource-intensive sites.
* **Turn off unnecessary features** — [Headful mode](/docs/main/02-features/stealth-mode-captcha-solving-proxies#headful-mode), [stealth mode](/docs/main/02-features/stealth-mode-captcha-solving-proxies#stealth-mode), and [proxies](/docs/main/02-features/stealth-mode-captcha-solving-proxies#proxies) add overhead. Disable them in `intuned.json` if your automation works without them.

## Additional tips

* **Build URLs directly** — Instead of clicking through filters, build the final URL with query parameters (e.g., `?category=electronics&price=under-100`).
* **Go to iframe URLs directly** — Instead of using `frameLocator`, navigate directly to the iframe's source URL to avoid loading the parent page.
* **Use `fill()` instead of `pressSequentially()`** — `pressSequentially()` types character-by-character. Use `fill()` for instant input unless you need keystroke events.
* **Avoid returning large data from `evaluate()`** — Extract only the fields you need, not entire DOM elements or large HTML strings.

## Related resources

<CardGroup>
  <Card title="Optimize cost" icon="dollar-sign" href="/main/03-how-to/solve/optimize-cost">
    Reduce compute and AI costs
  </Card>

  <Card title="Observability and logs" icon="chart-line" href="/main/02-features/observability-monitoring-logs">
    Debug with trace viewer
  </Card>

  <Card title="AuthSessions" icon="key" href="/main/02-features/auth-sessions">
    Reuse authenticated browser state
  </Card>
</CardGroup>


# How to optimize your Intuned cost
Source: https://intunedhq.com/docs/main/03-how-to/solve/optimize-cost



Intuned's cost is based on several usage metrics:

* **[Compute hours](/docs/main/02-features/usage-pricing-monitoring-alerting#usage-metrics-reference)**—Time your machines spend executing APIs
* **[AI spend](/docs/main/02-features/usage-pricing-monitoring-alerting#usage-metrics-reference)**—Dollars spent on AI processing
* **[JobRuns](/docs/main/02-features/usage-pricing-monitoring-alerting#usage-metrics-reference)**—Number of JobRuns executed
* **[Machine size surcharge](/docs/main/02-features/usage-pricing-monitoring-alerting#usage-metrics-reference)**—Additional cost for large/x-large machines

Visit [Usage and billing](/docs/main/02-features/usage-pricing-monitoring-alerting) to learn how to monitor your consumption and set limits.

Check each project's breakdown on the [Usage page](https://app.intuned.io/usage) to identify which metrics to optimize.

Below are ideas for optimizing each metric.

## Compute hours

### Speed up individual runs

The faster your automation runs, the less compute time you use. Follow the techniques in [Make automations faster](/docs/main/03-how-to/solve/make-automations-faster) to reduce execution time—including resource blocking, network interception, and batching evaluate calls.

### Reduce retries

When an automation fails repeatedly, it exhausts all retry attempts before failing. Each retry consumes compute time for a run that ultimately fails. Fix transient issues instead of relying on retries to work around them.

<Tip>
  Use the [trace viewer](/docs/main/02-features/observability-monitoring-logs#inspect-run-attempts) to debug failed runs and identify the root cause of errors.
</Tip>

### Combine related operations

Each API run has startup overhead—loading the browser, navigating to the site, and rendering assets. Reduce this by grouping related operations:

* **Pagination** — Instead of running one API per page, scrape multiple pages in a single run since the browser is already loaded.
* **Related tasks** — If you have several small APIs that hit the same site, consider merging them to avoid reloading the site for each one.

### Speed up AuthSession checks

If you have [AuthSessions](/docs/main/02-features/auth-sessions) enabled, the check API runs before every API call to verify the session is still valid. A slow check API adds overhead to every run. Keep your check API fast by:

* Only checking for essential authentication indicators
* Using simple selectors that resolve quickly
* Avoiding unnecessary navigation or waiting

The techniques in [Make automations faster](/docs/main/03-how-to/solve/make-automations-faster) also apply to your check API.

## AI spend

### Use cheaper models

Different AI models have different costs per token. When using [AI SDK methods](/docs/automation-sdks/intuned-sdk/typescript/ai/functions/extractStructuredData), specify a cheaper model if your task doesn't require the most capable one.

### Use Intuned SDK methods with built-in caching

Intuned SDK methods like [`extractStructuredData`](/docs/automation-sdks/intuned-sdk/typescript/ai/functions/extractStructuredData) have caching built in to avoid redundant LLM calls for identical inputs. Enable caching when processing similar pages.

### Use deterministic code when you can

If you're using AI for tasks that could be done with selectors and JavaScript, replace them. AI extraction is useful for unstructured or unpredictable content, but costs add up for predictable patterns. See [Replace AI code with deterministic code](/docs/main/03-how-to/solve/make-automations-faster#replace-ai-code-with-deterministic-code).

## JobRuns

### Reduce Job frequency

Check how often your [Jobs](/docs/main/02-features/jobs-batched-executions) run. If you're polling for data that rarely changes, reduce the schedule frequency.

### Skip unchanged items

If your Job processes multiple items but only some change between runs, skip items you've already scraped. Use the [KV cache](/docs/main/01-learn/recipes/kv-cache) to track what's been processed and detect changes. This reduces the number of payloads processed per JobRun.

### Tune concurrency

Your [replication settings](/docs/main/05-references/intuned-json#replication) control how many machines run in parallel. `maxCount` caps how far your project scales under load—more machines running at once means faster completion but higher peak cost, since you're billed for active compute across all busy machines simultaneously. `minCount` keeps machines deployed and ready, but idle machines aren't billed, so a higher baseline doesn't add cost when nothing is running (it does count toward your workspace's machine limit). If speed isn't critical, lower `maxCount` to spread load over time.

## Machine size surcharge

Large and x-large machines cost more per hour. You typically need them for:

* JavaScript-heavy websites that consume lots of memory
* Automations that need to be very fast
* Automations with long-running browser sessions that accumulate memory usage
* Automations that require [headful mode](/docs/main/02-features/stealth-mode-captcha-solving-proxies#headful-mode) or [stealth mode](/docs/main/02-features/stealth-mode-captcha-solving-proxies#stealth-mode)

To reduce machine size requirements:

* Split long-running APIs into smaller chunks that process data in batches
* Follow the techniques in [Make automations faster](/docs/main/03-how-to/solve/make-automations-faster) to reduce resource usage
* Disable [headful mode](/docs/main/02-features/stealth-mode-captcha-solving-proxies#headful-mode) and [stealth mode](/docs/main/02-features/stealth-mode-captcha-solving-proxies#stealth-mode) if not needed

## Upgrade your plan for better rates

Upgrading from the Developer plan to the Startup plan gives you lower overage rates:

| Metric                    | Developer   | Startup     |
| ------------------------- | ----------- | ----------- |
| Compute overage           | \$0.12/hour | \$0.10/hour |
| JobRun overage            | \$0.12/run  | \$0.10/run  |
| Large machine surcharge   | \$0.18/hour | \$0.15/hour |
| X-Large machine surcharge | \$0.48/hour | \$0.40/hour |

If your monthly overages are significant, the Startup plan may pay for itself through reduced rates. See [Plans and billing](/docs/main/05-references/plans-and-billing) for full pricing details.

## Need help optimizing?

If your costs are higher than expected, [reach out to us](/docs/main/06-resources/help-and-support). We can review your usage patterns and suggest optimizations, or discuss custom plans for high-volume workloads.

## Related resources

<CardGroup>
  <Card title="Make automations faster" icon="bolt" href="/main/03-how-to/solve/make-automations-faster">
    Reduce execution time and compute costs
  </Card>

  <Card title="Usage and billing" icon="chart-line" href="/main/02-features/usage-pricing-monitoring-alerting">
    Monitor consumption and set limits
  </Card>

  <Card title="Plans and billing" icon="credit-card" href="/main/05-references/plans-and-billing">
    Plan features and pricing details
  </Card>
</CardGroup>


# How to QA automation results
Source: https://intunedhq.com/docs/main/03-how-to/solve/qa-automation-results



## Overview

After building or updating an automation, you need to verify it works as expected. This guide shows you how to quickly QA your automation results using the Intuned dashboard.

You can consume a deployed project via [Runs](/docs/main/02-features/runs-single-executions) (single executions) or [Jobs](/docs/main/02-features/jobs-batched-executions) (batched executions).

## QA Runs

After triggering a Run, a new record appears in the **Runs** tab of your project.

<Steps>
  <Step title="Navigate to the Runs tab">
    From your project, open the **Runs** tab. You see a list of all Runs with key information like API name, start time, duration, and status.

    <img alt="Runs tab showing API executions" />
  </Step>

  <Step title="Find your Run">
    Look for the Run using the Run ID returned from your API call. Use the filters at the top to search by Run ID, API name, or status.

    <img alt="Filtering runs by run ID" />
  </Step>

  <Step title="View Run details">
    Select the Run record to open the details page. This shows the complete information about that Run.

    <img alt="Run details page showing results" />
  </Step>

  <Step title="Check the results">
    Review the JSON results in the **Result** tab to ensure the API returned the desired response. Verify that:

    * All expected fields are present
    * Data values are correct
    * The structure matches your expectations
  </Step>
</Steps>

## QA JobRuns

<Steps>
  <Step title="Navigate to the Jobs tab">
    From your project, open the **Jobs** tab. You see a list of all Jobs with a **Recent Job Runs** column showing quick stats for the latest JobRuns. Select the Job you want to QA.

    <img alt="Jobs tab showing jobs list with recent runs" />
  </Step>

  <Step title="View JobRuns">
    This opens the Job detail page showing all JobRuns with key metrics:

    * **Duration** — How long each JobRun took
    * **Payloads** — Total number of items processed
    * **Success/Failure counts** — Quick health check for each Run
    * **Status** — Whether the Run is Pending, Running, Completed, or Failed

    <img alt="Job runs page showing all runs for this job" />

    From the JobRuns page, you can quickly compare the current Run with previous Runs by looking at:

    * **Duration** — Significant differences may indicate performance issues or changes in source data
    * **Failure rate** — High failure counts compared to previous Runs suggest a problem
  </Step>

  <Step title="View individual JobRun details">
    Select a JobRun row to see all the individual Runs executed as part of that Job.

    <img alt="All API runs for the selected job run" />
  </Step>

  <Step title="Check the results">
    Follow these steps to verify your scraper extracted data correctly:

    1. **Verify completeness** — Confirm you captured all expected data:

       * If your scraper extends APIs (e.g., a list API triggering detail calls), filter the table to find the extending API and check the **Extended Payloads** tab to verify items from both the beginning and end of the page were extracted

       <img alt="Filtering runs by API name" />

       * If results are in a single Run, check the **Result** tab to verify all expected records are present

    2. **Check the count** — Verify the number of items matches your expectations:
       * For extended APIs: Look at how many detail Runs were triggered (should match the number of items on the page)
       * For single-Run results: Count the items in the JSON array

    3. **Spot check individual Runs** —
       * If your automation has multiple Runs, select several random individual Runs to verify they contain the correct attributes and values
       * If results are in a single Run, spot check random items in the JSON array

    <img alt="Run results showing JSON output" />

    <Tip>**Extended APIs** occur when one API triggers multiple nested API calls. For example, a **list** API might trigger **details** API calls for each item in the list.</Tip>
  </Step>
</Steps>

## Access Run details

Both standalone Runs and Runs within JobRuns share the same Run details page, which provides powerful debugging tools for investigating issues. For a comprehensive overview of debugging strategies, see [how to debug automations](/docs/main/03-how-to/solve/debug-automations).

**To access Run details from a Job:**

1. Navigate to **Jobs** tab
2. Select a Job to view JobRuns
3. Select a JobRun row to see the Runs table
4. Select a Run row to expand details
5. Select the **Run ID** to open the full Run details page

<img alt="Accessing run details page from a job" />

**To access Run details (standalone):**

* Select any Run ID from the **Runs** tab

### Run in IDE

If you need to debug a failed Run or investigate unexpected results, you can re-run it in the IDE with the exact same input parameters:

1. **Open the Run details page** — Select the Run ID to open the detailed view
2. **Select the Play icon** — In the Run details page, select the **Play** icon to launch **Run in IDE**
3. **Review the execution** — The [online IDE](/docs/main/02-features/online-ide) opens with the Run's parameters pre-loaded and starts executing

<img alt="Running in IDE" />

This is especially useful for:

* Debugging failed Runs
* Investigating why certain results don't match expectations
* Testing fixes before redeploying

<Note>
  **Run in IDE** only works with [Hosted
  projects](/docs/main/02-features/online-ide).
</Note>

### View trace in Playwright trace viewer

For deeper debugging, you can view detailed execution traces in Playwright's trace viewer:

1. **Open the Run details page** — Select any Run ID to open the detailed view
2. **Scroll down to the Attempts Timeline** — Find the attempts section showing all retry attempts for that Run
3. **Select an attempt** — Select any attempt to expand it, revealing additional details
4. **Select View Trace** — Select **View Trace** to open the Run in Playwright's trace viewer

<img alt="Viewing Playwright traces" />

The trace viewer shows detailed debugging information including screenshots, network activity, console logs, and step-by-step execution flow. This is especially useful for debugging complex failures and understanding exactly what happened during execution.

## Red flags to watch for

When reviewing JobRuns, compare the current JobRun against previous JobRuns to identify issues:

* **Sudden drop in total Runs** — If the current JobRun has significantly fewer Runs compared to previous JobRuns (e.g., 50 Runs instead of the usual 200), this indicates missing data or pagination issues. Check if your list scraper correctly extracts all items.

* **High failure rate** — If the failure count is much higher than previous JobRuns (e.g., 30% failures vs the usual 2%), this suggests source website changes, bot detection, or code errors. Review failed Runs to identify patterns.

* **Significantly longer duration** — If the JobRun takes much longer than previous Runs (e.g., 10 minutes vs the usual 2 minutes), this indicates performance degradation or timeout issues. Check if the source website is slower or if your code has inefficiencies.

* **Missing first/last items** — When spot checking extended Runs, if you don't see items from the beginning or end of the expected list, this indicates incomplete page scraping. Your pagination or scrolling logic may not be working correctly.

* **Inconsistent attribute extraction** — If some Runs are missing attributes that were previously extracted, or returning null/undefined values, this suggests the source website's schema changed or your selectors need updating.


# Structure your Intuned project
Source: https://intunedhq.com/docs/main/03-how-to/solve/structure-intuned-projects



Intuned is flexible—there are many ways to structure automations into projects. Here are common patterns to guide you.

## Project patterns

### Single-site scraper

Scrape entities from one website using a list-details pattern.

**Best for:** Entity scrapers—product catalogs, job postings, event listings, public records.

**Structure:**

```
my-scraper/
├── api/
│   ├── list           # Scrape paginated list
│   └── details        # Scrape individual item
├── helpers/
│   ├── transformers   # Date parsing, data cleanup
│   └── schema         # Shared data models
├── Intuned.json
└── pyproject.toml
```

| API       | Purpose                           | Parameters                          |
| --------- | --------------------------------- | ----------------------------------- |
| `list`    | Scrape the list page              | Optional filters, `offset`, `limit` |
| `details` | Scrape extended data for one item | Item URL or ID from list            |

The `list` API returns lightweight records. The `details` API fetches everything else: images, full descriptions, related items, or data requiring interaction.

### Automate a site without an API (RPA)

Build an integration to a site that lacks an API—or extend what exists.

**Best for:** Insurance portals, government websites, or any legacy website that has no official APIs.

**Structure:**

```
insurance-portal/
├── api/
│   ├── get_claims        # List existing claims
│   ├── get_claim_status  # Check claim status
│   ├── submit_claim      # File a new claim
│   ├── upload_document   # Attach documents
│   └── download_eob      # Download explanation of benefits
├── helpers/
│   ├── navigation        # Common navigation steps
│   ├── patient           # Select patient flow
│   └── schema            # Data models
├── Intuned.json
└── pyproject.toml
```

| API               | Purpose                     | Parameters                     |
| ----------------- | --------------------------- | ------------------------------ |
| `get_claims`      | List claims                 | `patient_id`, optional filters |
| `submit_claim`    | File new claim              | `patient_id`, `claim_data`     |
| `upload_document` | Attach file to claim        | `claim_id`, `document_url`     |
| `download_eob`    | Get explanation of benefits | `claim_id`                     |

<Tip>
  This pattern typically uses [AuthSessions](/docs/main/02-features/auth-sessions). Users log in once, and you'll reuse the session across API calls.
</Tip>

### Crawler

Crawl websites to extract content. Use this when a structured scraper isn't a good fit—either the site has no clear structure, or you need something that works across multiple sites.

**Best for:** AI/LLM data pipelines, search indexes, content aggregation, site archiving, knowledge bases.

**Structure:**

```
web-crawler/
├── api/
│   ├── map        # Discover all URLs on a site
│   ├── scrape     # Extract content from single URL
│   ├── crawl      # Crawl and extract from subpages
│   └── search     # Search the web, get content
├── helpers/
│   ├── parser     # HTML to markdown, content extraction
│   └── schema     # Output schemas
├── Intuned.json
└── pyproject.toml
```

| API      | Purpose                              | Parameters                                  |
| -------- | ------------------------------------ | ------------------------------------------- |
| `map`    | Discover all URLs on a site (fast)   | `start_url`, `limit`                        |
| `scrape` | Extract content from single URL      | `url`, `formats` (markdown, html, metadata) |
| `crawl`  | Crawl site and extract from subpages | `start_url`, `max_depth`, `limit`           |
| `search` | Search the web, return full content  | `query`, `limit`                            |

**How it works:**

* `map` quickly returns all discoverable URLs without extracting content.
* `scrape` handles a single page—returns markdown, HTML, metadata.
* `crawl` combines discovery and extraction for an entire site.
* `search` performs web search and scrapes results.

### Multi-site template scraper

Scrape multiple sites that share the same structure—typically from a platform that generates sites for clients.

**Best for:** Shopify stores (e-commerce), Lever/Greenhouse boards (job postings), franchise websites, sites built on the same CMS.

**Structure:**

```
shopify-scraper/
├── api/
│   ├── list           # Scrape product list from any store
│   └── details        # Scrape product details
├── helpers/
│   ├── transformers   # Price parsing, variant handling
│   └── schema         # Unified product schema
├── Intuned.json
└── pyproject.toml
```

| API       | Purpose                | Parameters                   |
| --------- | ---------------------- | ---------------------------- |
| `list`    | Scrape product list    | `site_url`, optional filters |
| `details` | Scrape product details | `site_url`, `product_url`    |

**Output:** Consistent schema across all sites. A product from store A has the same structure as one from store B.

<Tip>
  If sites diverge significantly in structure, split them into separate projects. This pattern works best when the template is consistent.
</Tip>

### Nested category scraper

Scrape sites with nested navigation: categories, subcategories, items.

**Best for:** E-commerce catalogs, classified ads, business directories.

**Structure:**

```
ecommerce-scraper/
├── api/
│   ├── get_categories    # Get top-level categories
│   ├── list              # Get items in a category
│   └── details           # Get product details
├── helpers/
│   ├── transformers      # Price, variant parsing
│   └── schema            # Product schema
├── Intuned.json
└── pyproject.toml
```

| API              | Purpose                          | Parameters                 |
| ---------------- | -------------------------------- | -------------------------- |
| `get_categories` | Get all categories/subcategories | Optional `parent_category` |
| `list`           | Get items in a category          | `category_url`, pagination |
| `details`        | Get full product data            | `product_url`              |

Start with `get_categories` to discover the lists, then `list` for each list, then `details` for each item.

### AI agent automation

Use AI to automate workflows across unknown or varied websites. The target site isn't known in advance—the AI adapts at runtime.

**Best for:** Multi-provider comparisons, cross-site data aggregation, generic web tasks where building per-site scrapers isn't feasible.

**Structure:**

```
insurance-quotes/
├── api/
│   ├── find_providers       # Search for top providers
│   ├── get_quote            # Get quote from any provider site
│   └── download_quote_pdf   # Download quote document
├── helpers/
│   ├── agent                # AI agent setup (Browser-Use, Stagehand)
│   └── schema               # Unified quote schema
├── Intuned.json
└── pyproject.toml
```

| API                  | Purpose                               | Parameters                |
| -------------------- | ------------------------------------- | ------------------------- |
| `find_providers`     | Search for providers                  | `query`, `count`          |
| `get_quote`          | Navigate to site, find form, submit   | `site_url`, `car_details` |
| `download_quote_pdf` | Download quote document from provider | `site_url`, `quote_id`    |

**How it works:** AI agents (like [Browser-Use](/docs/main/04-integrations/browser-use) or [Stagehand](/docs/main/04-integrations/stagehand)) handle navigation, form discovery, and data extraction. The same API works on sites it's never seen before.

<Tip>
  Use this pattern when target sites are unknown or too numerous to build individual scrapers. For known, stable sites, deterministic scrapers are more reliable.
</Tip>

### Utility project

Simple, focused APIs. Building blocks for larger systems.

**Best for:** Markdown extraction, PDF collection, screenshot services, page archiving.

**Structure:**

```
web-utilities/
├── api/
│   ├── get_markdown     # Convert page to markdown
│   ├── get_pdfs         # Extract PDF links/attachments
│   ├── screenshot       # Capture page screenshot
│   └── archive_page     # Save full page content
├── Intuned.json
└── pyproject.toml
```

| API            | Purpose                           | Parameters                     |
| -------------- | --------------------------------- | ------------------------------ |
| `get_markdown` | Convert page to markdown          | `url`, `include_images`        |
| `get_pdfs`     | Extract all PDF links/attachments | `url`                          |
| `screenshot`   | Capture page screenshot           | `url`, `viewport`, `full_page` |
| `archive_page` | Save full page content            | `url`, `format`                |

<Tip>
  Keep utilities minimal and predictable. They compose well into larger workflows.
</Tip>

### Multi-purpose project

Consolidate many APIs into one project. Use nested folders to organize. Teams typically manage these projects with the [Intuned CLI](/docs/main/02-features/local-development-cli) for local development and deploy via CI/CD pipelines.

**Best for:** Automations that share significant logic and helpers (without helper packages), and organizations wanting to manage multiple different purpose automations in one project.

**Structure:**

```
company-automations/
├── api/
│   ├── my-scraper/
│   │   ├── list
│   │   └── details
│   ├── insurance-portal/
│   │   ├── get_claims
│   │   ├── submit_claim
│   │   └── download_eob
│   ├── web-crawler/
│   │   ├── map
│   │   ├── scrape
│   │   └── crawl
│   └── markdown-extractor/
│       └── get_markdown
├── helpers/
│   ├── schema            # Shared schemas
│   ├── parsers           # Common parsers
│   └── transformers      # Data transformers
├── Intuned.json
└── pyproject.toml
```

**API naming:** Folders become part of the API name. `my-scraper/list` is the name of the API here.

**Workflow:**

1. Develop locally using [Intuned CLI](/docs/main/02-features/local-development-cli).
2. Source control with git.
3. Deploy with CI/CD (GitHub Actions) using `intuned deploy`.

<Warning>
  Use this pattern with care. Consolidating too much into one project means all APIs deploy together, share configuration, and can't scale independently. Consider whether separate projects would be cleaner—the same tradeoffs apply as putting all code in one monorepo vs. splitting into focused repositories.
</Warning>

## Design guidelines

Whatever pattern you choose, keep these principles in mind:

### One project, one purpose

Everything in a project deploys together and shares configuration (AuthSessions, secrets, settings).

**Split** when automations target different sites, have different auth requirements, or change independently.

**Combine** when automations work on the same platform, share authentication, or share significant code.

<Tip>
  If the same site exposes more than one login flow (for example, separate provider and patient logins on a healthcare portal), keep it in one project. Pass a parameter when creating each AuthSession that identifies the flow, branch on it inside `create`, and read it back inside `check` with [`getAuthSessionParameters`](/docs/main/05-references/runtime-sdk-typescript/get-auth-session-parameters). See [Multiple login flows on the same site](/docs/main/02-features/auth-sessions#usage-patterns).
</Tip>

### Small, focused APIs

Each API should perform one logical unit of browser work.

| Benefit         | Explanation                              |
| --------------- | ---------------------------------------- |
| Retryability    | If step 3 fails, retry step 3—not all 10 |
| Parallelization | Fan out thousands of runs with Jobs      |
| Observability   | Monitor and debug at the run level       |

A good API does one thing, succeeds or fails independently, and is retriable with the same parameters.


# 1Password
Source: https://intunedhq.com/docs/main/04-integrations/1password



## Overview

By the end of this guide, you'll have an authenticated Intuned project that uses credentials stored securely in 1Password. You'll learn how to:

1. Set up a 1Password account and create a vault for Intuned to access.
2. Set up a 1Password Service Account to access your vault programmatically.
3. Enable and configure 1Password integration in your Intuned project.
4. Create AuthSessions using credentials from 1Password.

## Prerequisites

Before you begin, ensure you have the following:

* An active Intuned account.
* A 1Password account with permissions to create vaults and service accounts.

<Note>This guide assumes you have a basic understanding of Intuned Projects and AuthSessions. If you're new to Intuned, start with the [getting started guide](/docs/main/00-getting-started/introduction).</Note>

## When to use 1Password integration

When you create an AuthSession, you provide credentials for logging into the target website. Intuned stores these credentials securely to renew the AuthSession when it expires. However:

* You may prefer not to store sensitive credentials directly in Intuned.
* If credentials change frequently, you need to update them manually through Intuned APIs.
* Sharing credentials across Projects or AuthSessions requires duplicating them.

With 1Password integration, you can:

* Keep credentials in 1Password instead of Intuned.
* Fetch the latest credentials automatically when needed.
* Share credentials across Projects and AuthSessions without duplication.

<Note>1Password integration only works in deployed environments. During local development (IDE or CLI), use test credentials instead.</Note>

## Guide

### 1. Create a 1Password Vault with credentials

<Tabs>
  <Tab title="1Password App (Desktop)">
    1. Open the 1Password app and sign in to your account.
    2. Under Vaults, select **+**.
    3. Enter name: `intuned-automation-credentials` and optionally a description.
    4. Select **Create**.
    5. Add a new item:
       * Select **Login**
       * Name it `example-login`
       * Username: `demo@email.com`
       * Password: `DemoUser2024!`
  </Tab>

  <Tab title="1password.com">
    1. Go to [1password.com](https://1password.com) and sign in to your account.
    2. From your account home page, select **+ New Vault**.
    3. Enter name: `intuned-automation-credentials` and optionally a description.
    4. Select **Create Vault**.
    5. Add a new item to the vault:
       * Select **Login**
       * Name it `example-login`
       * Username: `demo@email.com`
       * Password: `DemoUser2024!`
  </Tab>

  <Tab title="1Password CLI">
    1. Install the latest version of [1Password CLI](https://developer.1password.com/main/cli/get-started/) and sign in to your account.

    ```bash theme={null}
    op signin
    ```

    2. Create a vault:

    ```bash theme={null}
    op vault create "intuned-automation-credentials" --description "Vault for Intuned AuthSession credentials"
    ```

    3. Add item to the vault:

    ```bash theme={null}
    op item create \
      --vault='intuned-automation-credentials' \
      --title='example-login' \
      --category=login \
      "username=demo@email.com" \
      "password[password]=DemoUser2024!"
    ```
  </Tab>
</Tabs>

You now have two 1Password items: `op://intuned-automation-credentials/example-login/username` and `op://intuned-automation-credentials/example-login/password`. You'll use these paths later to reference the credentials in Intuned.

<Tip>For more details, refer to the official [1Password documentation on creating vaults](https://support.1password.com/create-share-vaults).</Tip>

### 2. Create a 1Password Service Account

<Note>Creating a service account requires admin access to your 1Password account.</Note>

<Tabs>
  <Tab title="1password.com">
    1. Open the [1Password service account creation wizard](https://start.1password.com/developer-tools/infrastructure-secrets/serviceaccount/).
    2. Follow the instructions:
       1. Enter a name for your service account.
       2. Choose the vault you created earlier.
       3. Make sure you give read access to the vault.
       4. Select **Create Account**.
       5. (Optional) Select **Save in 1Password** to save the service account token in your 1Password account.

    You can later access your service account under **Developer** from your 1Password account home screen.

    <Warning>Copy the service account token and store it securely (preferably in 1Password). You won't be able to view it again.</Warning>
  </Tab>

  <Tab title="1Password CLI">
    1. Create a service account and assign it to your vault:

    ```bash theme={null}
    op service-account create "<service-account-name>" \
      --vault "intuned-automation-credentials:read_items"
    ```

    2. Copy the service account token.

    <Warning>Copy the service account token and store it securely (preferably in 1Password). The 1Password CLI returns it only once.</Warning>
  </Tab>
</Tabs>

<Tip>For more details, refer to the official [1Password documentation on creating service accounts](https://developer.1password.com/main/service-accounts/get-started/).</Tip>

<Note>1Password Service Accounts have rate limits. Intuned reads from the vault only when creating or renewing AuthSessions. Check out [1Password service account rate limits](https://developer.1password.com/main/service-accounts/rate-limits/) for more details.</Note>

### 3. Set up 1Password Service Account Token on Intuned

Set up your 1Password Service Account token as a Workspace Environment Variable:

1. Go to [**Workspace Settings** > **Environment Variables**](https://app.intuned.io/settings/env-vars).
2. Create a new variable with:
   * Key: `OP_SERVICE_ACCOUNT_TOKEN`
   * Value: Your 1Password Service Account token from step 2.
   * Environments: Deployed only.

<img alt="Set up 1Password Service Account token as Environment Variable" />

<Note>You can set up the token as a Project Environment Variable if you do not want to share the 1Password Service Account across Projects. Check out [Project-level environment variables](/docs/main/02-features/environment-variables-secrets#project-level-environment-variables) for more details.</Note>

<Note>For security reasons, `OP_SERVICE_ACCOUNT_TOKEN` will not be exposed to your automation code. If you want to use the 1Password SDK in your code, use a different environment variable key at your own risk.</Note>

### 4. Create a Project with AuthSessions and 1Password integration enabled

For this guide, we use the **Book consultations authenticated quickstart project** which is the `rpa-auth-example` template. Follow the [quickstart guide](/docs/main/00-getting-started/quickstarts/auth-rpa) to create it. You should now have a project named `book-consultations-authenticated-quickstart`.

Let's enable 1Password integration for the Project and deploy it.

<Tabs>
  <Tab title="Online IDE">
    Open your project in the IDE. From **Files**, select **Intuned.json**, then enable 1Password integration. Optionally add an integration name and version.

    <img alt="Enable 1Password in the Online IDE" />
  </Tab>

  <Tab title="CLI">
    In your local project folder, open your Intuned settings file and enable 1Password integration. Optionally add an integration name and version.

    <CodeGroup>
      ```jsonc Intuned.jsonc theme={null}
      {
        // rest of settings
        "integrations": { // [!code ++:7]
          "onepassword": {
            "enabled": true,
            "integrationName": "<optional-integration-name>", 
            "integrationVersion": "<optional-integration-version>"
          }
        }
      }
      ```

      ```jsonc Intuned.json theme={null}
      {
        // rest of settings
        "integrations": { // [!code ++:7]
          "onepassword": {
            "enabled": true,
            "integrationName": "<optional-integration-name>", 
            "integrationVersion": "<optional-integration-version>"
          }
        }
      }
      ```
    </CodeGroup>
  </Tab>
</Tabs>

<Note>The `integrationName` and `integrationVersion` fields are optional identifiers used by the 1Password SDK for tracking.</Note>

**Deploy your project** for the changes to take effect.

### 5. Create an AuthSession using 1Password credentials

<Tabs>
  <Tab title="API">
    <CodeGroup>
      ```typescript Typescript theme={null}
      await intuned.projects.authSessions.create.start(
        "book-consultations-authenticated-quickstart",
        {
          id: "1password-example",
          parameters: {
            "username": "op://intuned-automation-credentials/example-login/username",
            "password": "op://intuned-automation-credentials/example-login/password"
          }
        },
      );

      ```

      ```python Python theme={null}
      from intuned_client import models

      intuned.projects.auth_sessions.create.start(
        project_name="book-consultations-authenticated-quickstart",
        body=models.AuthSessionsCreateStartRequestBody(
          id="1password-example",
          parameters={
            "username": "op://intuned-automation-credentials/example-login/username",
            "password": "op://intuned-automation-credentials/example-login/password"
          }
        )
      )

      ```
    </CodeGroup>
  </Tab>

  <Tab title="Dashboard">
    From **your project** > **AuthSessions**, select **+ Session**. Let's name it `1password-example`. When you enter the parameters, enter the 1Password credential path instead of the actual credentials.

    <CodeGroup>
      ```json JSON theme={null}
      {
        "username": "op://intuned-automation-credentials/example-login/username",
        "password": "op://intuned-automation-credentials/example-login/password"
      }
      ```

      ```yaml YAML theme={null}
      username: op://intuned-automation-credentials/example-login/username
      password: op://intuned-automation-credentials/example-login/password
      ```

      Intuned will securely fetch the credentials from 1Password and pass them to your `auth-sessions/create` function to create the AuthSession.
    </CodeGroup>

    <img alt="Create AuthSession with 1Password paths as credentials" />
  </Tab>
</Tabs>

### 6. Use the AuthSession in your API

<Tabs>
  <Tab title="API">
    <CodeGroup>
      ```typescript Typescript theme={null}
      await intuned.projects.runs.start(
        projectName,
        {
          api: "book-consultations",
          parameters: {
            name: "Foo Bar",
            email: "foo@bar.com",
            phone: "1234567890",
            date: "2026-11-19",
            time: "10:00 AM",
            topic: "other"
          },
          authSession: {
            id: "1password-example",
          },
        }
      );

      ```

      ```python Python theme={null}
      from intuned_client import models

      intuned.projects.runs.start(
        project_name=project_name,
        body=models.RunStartRequestBody(
          api="book-consultations",
          parameters={
            "name": "Foo Bar",
            "email": "foo@bar.com",
            "phone": "1234567890",
            "date": "2026-11-19",
            "time": "10:00 AM",
            "topic": "other"
          },
          auth_session={
            "id": "1password-example"
          },
        ),
      )

      ```
    </CodeGroup>
  </Tab>

  <Tab title="Dashboard (Playground)">
    From the dashboard, go to **your project** > **Runs** > **Start Run**. Select `book-consultations`, enter the parameters:

    ```json theme={null}
    {
      "name": "Foo Bar",
      "email": "foo@bar.com",
      "phone": "1234567890",
      "date": "2026-11-19",
      "time": "10:00 AM",
      "topic": "other"
    }
    ```

    Select the AuthSession `1password-example` you created earlier. Select **Start Run**.

    <img alt="Use 1Password Auth Session in Run Playground" />
  </Tab>
</Tabs>

## Related resources

<CardGroup>
  <Card title="Deploy your first authenticated RPA" icon="rocket" href="/main/00-getting-started/quickstarts/auth-rpa">
    Step-by-step guide to creating your first authenticated RPA project
  </Card>

  <Card title="AuthSessions" icon="key" href="/main/02-features/auth-sessions">
    Learn more about AuthSessions in Intuned
  </Card>
</CardGroup>


# Browser Use
Source: https://intunedhq.com/docs/main/04-integrations/browser-use



## Overview

[Browser Use](https://browser-use.com/) is a Python library for AI-powered browser automation.

This guide walks you through deploying and running a Browser Use based project on Intuned. You'll build a sample automation that purchases a product from an e-commerce site—without writing step-by-step Playwright code. The same patterns apply to any automation you build with the framework.

<Note>
  This guide assumes you have a basic understanding of Intuned projects. If
  you're new to Intuned, start with the [getting started
  guide](/docs/main/00-getting-started/introduction).
</Note>

<Note>Browser Use is Python-only and doesn't support TypeScript.</Note>

## When to use AI automation

Intuned supports AI-powered browser automation frameworks like Browser Use, Stagehand, and others. Use AI automation when:

* **Pages are dynamic** — Elements change position, structure, or content unpredictably
* **You don't know the exact page structure** — Automating sites you haven't mapped in detail
* **You want natural language control** — Describe what to do instead of writing precise selectors
* **Traditional Playwright code is too brittle for your case** — AI agents adapt to minor UI changes automatically

Browser Use is one option for AI automation on Intuned. The setup patterns in this guide apply to other AI frameworks as well.

For a deeper dive into choosing between deterministic, AI-driven, and hybrid approaches, check out [Flexible Automations](/docs/main/02-features/flexible-automation).

## Guide

Let's build a `purchase-item` API with AI agents. The template handles all the Browser Use setup for you.

You can start with Browser Use in two ways:

* **Online IDE** — Write, test, and deploy directly from your browser.
* **Local CLI** — Use your favorite editor and version control workflow.

<Tabs>
  <Tab title="Online IDE">
    <Steps>
      <Step title="Create a project">
        1. Go to [Intuned dashboard](https://app.intuned.io)
        2. Select **+ New Project** > **Templates** > **Browser Use Template**
        3. Name it and select **Create Project**

        <Frame>
          <img alt="Create Browser Use project from template" />
        </Frame>
      </Step>

      <Step title="Explore the project">
        The template includes a `purchase-item` API that uses AI agents to purchase products from an e-commerce site.

        **Project structure:**

        ```
        my-browser-use-agent/
        ├── api/
        │   └── purchase-item.py           # Main automation API
        ├── hooks/
        │   └── setup_context.py           # Browser Use initialization
        ├── pyproject.toml                 # Dependencies
        └── intuned.json                   # Project configuration
        ```

        **The automation code:**

        ```python theme={null}
        from playwright.async_api import Page
        from typing import TypedDict
        from browser_use import Agent, ChatOpenAI, Browser, Tools
        from intuned_runtime import attempt_store


        class Params(TypedDict):
            username: str
            password: str
            product_name: str
            first_name: str
            last_name: str
            zip_code: str


        async def automation(page: Page, params: Params | None = None, **_kwargs):
            if params is None:
                raise Exception("params cannot be null")
            browser: Browser = attempt_store.get("browser")

            username = params.get("username")
            password = params.get("password")
            product_name = params.get("product_name")
            first_name = params.get("first_name")
            last_name = params.get("last_name")
            zip_code = params.get("zip_code")

            tools = Tools()

            agent = Agent(
                browser=browser,
                task=f"""1. Go to https://www.saucedemo.com/
        2. Login with username '{username}' and password '{password}'
        3. Find the product '{product_name}' and add it to cart
        4. Go to cart
        5. Proceed to checkout
        6. Fill in the checkout information: First Name: '{first_name}', Last Name: '{last_name}', Zip Code: '{zip_code}'
        7. Complete the purchase""",
                llm=ChatOpenAI(model="gpt-5-nano", temperature=0),
                flash_mode=True,
                tools=tools,
            )

            # Agent will run the task on current browser page
            # Since Browser-use uses CDP directly, the actions done by the agent won't be visible in the Playwright trace
            result = await agent.run()
            print(result)

            # Use the correct AgentHistoryList methods
            return {
                "success": result.is_successful(),
                "is_done": result.is_done(),
                "final_message": result.final_result() or "No result",
                "total_actions": result.number_of_steps(),
                "action_history": result.extracted_content(),
            }
        ```

        <Info>
          **What this does:** Uses an AI agent to autonomously purchase a product from an e-commerce site. Takes login credentials, product name, and checkout details, then navigates through the entire purchase flow using `flash_mode` for optimized performance. Returns detailed execution results. Agent actions won't appear in Playwright traces.
        </Info>
      </Step>

      <Step title="Run your automation">
        Run the AI agent to test your setup.

        1. In the IDE, select **purchase-item** from the API dropdown
        2. Enter the purchase parameters in the params panel
        3. Select **Run**

        <Frame>
          <img alt="Running Browser Use in IDE" />
        </Frame>
      </Step>

      <Step title="Deploy and test">
        Deploy your automation to Intuned's infrastructure.

        1. In the Online IDE, select **Deploy** in the top-right corner
        2. After deployment completes, go to the project's **Runs** page
        3. Select **Start Run**, then choose `purchase-item` API
        4. Enter purchase parameters and select **Start Run**

        <Frame>
          <img alt="Running Browser Use agent in Dashboard" />
        </Frame>

        5. After the run completes, view the purchase results

        <Frame>
          <img alt="View Browser Use run results" />
        </Frame>

        <Tip>
          Learn more about [Runs and how to trigger them programmatically](/docs/main/02-features/runs-single-executions).
        </Tip>
      </Step>
    </Steps>
  </Tab>

  <Tab title="Local CLI">
    <Steps>
      <Step title="Create a project">
        Run the following command and select **Python**, then choose the **Browser Use** template:

        ```bash theme={null}
        npx create-intuned-project@latest
        ```

        Then navigate to your project:

        ```bash theme={null}
        cd my-browser-use-agent
        ```
      </Step>

      <Step title="Explore the project">
        The template includes a `purchase-item` API that uses AI agents to purchase products from an e-commerce site.

        **Project structure:**

        ```
        my-browser-use-agent/
        ├── api/
        │   └── purchase-item.py           # Main automation API
        ├── hooks/
        │   └── setup_context.py           # Browser Use initialization
        ├── pyproject.toml                 # Dependencies
        └── intuned.json                   # Project configuration
        ```

        **The automation code:**

        ```python theme={null}
        from playwright.async_api import Page
        from typing import TypedDict
        from browser_use import Agent, ChatOpenAI, Browser, Tools
        from intuned_runtime import attempt_store


        class Params(TypedDict):
            username: str
            password: str
            product_name: str
            first_name: str
            last_name: str
            zip_code: str


        async def automation(page: Page, params: Params | None = None, **_kwargs):
            if params is None:
                raise Exception("params cannot be null")
            browser: Browser = attempt_store.get("browser")

            username = params.get("username")
            password = params.get("password")
            product_name = params.get("product_name")
            first_name = params.get("first_name")
            last_name = params.get("last_name")
            zip_code = params.get("zip_code")

            tools = Tools()

            agent = Agent(
                browser=browser,
                task=f"""1. Go to https://www.saucedemo.com/
        2. Login with username '{username}' and password '{password}'
        3. Find the product '{product_name}' and add it to cart
        4. Go to cart
        5. Proceed to checkout
        6. Fill in the checkout information: First Name: '{first_name}', Last Name: '{last_name}', Zip Code: '{zip_code}'
        7. Complete the purchase""",
                llm=ChatOpenAI(model="gpt-5-nano", temperature=0),
                flash_mode=True,
                tools=tools,
            )

            # Agent will run the task on current browser page
            # Since Browser-use uses CDP directly, the actions done by the agent won't be visible in the Playwright trace
            result = await agent.run()
            print(result)

            # Use the correct AgentHistoryList methods
            return {
                "success": result.is_successful(),
                "is_done": result.is_done(),
                "final_message": result.final_result() or "No result",
                "total_actions": result.number_of_steps(),
                "action_history": result.extracted_content(),
            }
        ```

        <Info>
          **What this does:** Uses an AI agent to autonomously purchase a product from an e-commerce site. Takes login credentials, product name, and checkout details, then navigates through the entire purchase flow using `flash_mode` for optimized performance. Returns detailed execution results. Agent actions won't appear in Playwright traces.
        </Info>
      </Step>

      <Step title="Run locally">
        Test your automation locally:

        <CLICommandTabs />

        The agent logs in, finds the product, and completes the purchase.
      </Step>

      <Step title="Deploy and test">
        Deploy your automation using the CLI:

        <CLICommandTabs />

        After deployment, test in the Dashboard:

        1. Go to the project's **Runs** page
        2. Select **Start Run**, then choose `purchase-item` API
        3. Enter purchase parameters and select **Start Run**

        <Frame>
          <img alt="Running Browser Use agent in Dashboard" />
        </Frame>

        4. After the run completes, view the purchase results

        <Frame>
          <img alt="View Browser Use run results" />
        </Frame>

        <Tip>
          Learn more about [Runs and how to trigger them programmatically](/docs/main/02-features/runs-single-executions).
        </Tip>
      </Step>
    </Steps>
  </Tab>
</Tabs>

## How it works

* The `setup_context` [hook](/docs/main/01-learn/recipes/setup-hooks) runs before your API executes. It creates a Browser instance using Intuned's CDP URL and stores it in [`attemptStore`](/docs/main/05-references/runtime-sdk-python/attempt-store)
* Your API retrieves the Browser instance from `attemptStore` and uses it to create AI agents
* Parameters passed to your API control the agent's behavior. Change these to adjust your automation flow.

<Tip>
  Want to combine Playwright with Browser Use? Use Playwright for deterministic
  steps and Browser Use for AI decision-making. Set `flash_mode=True` to
  optimize costs. Learn more in [Flexible
  Automations](/docs/main/02-features/flexible-automation) and [Structuring Projects
  with AI
  Agents](/docs/main/03-how-to/solve/structure-intuned-projects#ai-agent-automation).
</Tip>

## Related resources

<CardGroup>
  <Card title="Browser Use Documentation" icon="book" href="https://docs.browser-use.com">
    Official Browser Use documentation and API reference
  </Card>

  <Card title="Browser Use GitHub" icon="github" href="https://github.com/browser-use/browser-use">
    View the source code and contribute to Browser Use
  </Card>

  <Card title="Intuned Cookbook" icon="code" href="https://github.com/Intuned/cookbook/tree/main/python-examples/browser-use">
    Complete Browser Use example in the Intuned Cookbook
  </Card>

  <Card title="attemptStore Reference" icon="database" href="/main/05-references/runtime-sdk-python/attempt-store">
    Learn more about sharing data between hooks and APIs
  </Card>
</CardGroup>


# n8n
Source: https://intunedhq.com/docs/main/04-integrations/n8n



## Overview

[n8n](https://n8n.io/) is a workflow automation platform that connects services through a visual interface.

This guide walks you through integrating Intuned with n8n. You'll build a sales email workflow that crawls company websites and generates personalized outreach using AI—demonstrating how to call Intuned APIs from n8n workflows.

<Note>This guide assumes you're familiar with n8n basics. If you're new to Intuned, start with the [getting started guide](/docs/main/00-getting-started/introduction).</Note>

## Prerequisites

Before you begin, ensure you have the following:

* An [Intuned account](https://app.intuned.io) with API credentials
* An n8n instance (cloud or self-hosted)
* The [crawl4ai project](https://github.com/Intuned/cookbook) deployed to your workspace
* An OpenAI API key
* A Gmail account for sending emails

## What you can build

n8n + Intuned unlocks workflows that combine browser automation with hundreds of other services:

* **Sales outreach** — Crawl prospect websites, generate personalized emails with AI, send via Gmail or Outlook
* **Lead enrichment** — Scrape company pages for contact info and push directly to your CRM
* **Market research** — Monitor competitor pricing, store in Airtable or Google Sheets, alert on Slack when prices change
* **Content monitoring** — Watch pages for updates, extract changes, notify your team

This guide demonstrates a basic sales outreach pattern. The same approach applies to any workflow that needs browser automation as part of a larger process.

## Guide

### 1. Get your Intuned API credentials

<Steps>
  <Step title="Get your API key and workspace ID" icon="key">
    1. Go to [app.intuned.io](https://app.intuned.io)
    2. Navigate to **Settings** > **API Keys**
    3. Create a new API key or use an existing one
    4. Find your **Workspace ID** in [Workspace management](/docs/main/03-how-to/manage/manage-workspace)

    You'll use these credentials in the n8n HTTP Request nodes.
  </Step>
</Steps>

### 2. Import the n8n workflow

The example workflow demonstrates a complete automation pipeline: collecting form inputs, crawling a company website, generating a personalized sales email with AI, and sending it via Gmail.

<Steps>
  <Step title="Copy the workflow JSON" icon="copy">
    <Accordion title="Click to view and copy the workflow JSON">
      ```json theme={null}
      {
        "name": "Draft Sale Email Generator",
        "nodes": [
          {
            "parameters": {
              "formTitle": "Sales Email Generator",
              "formDescription": "Fill in the details to generate a personalized sales email",
              "formFields": {
                "values": [
                  {
                    "fieldLabel": "What are you selling?"
                  },
                  {
                    "fieldLabel": "Target company website"
                  },
                  {
                    "fieldLabel": "Job title (e.g., VP Engineering)"
                  },
                  {
                    "fieldLabel": "Your email address",
                    "fieldType": "email"
                  },
                  {
                    "fieldLabel": "Your name"
                  },
                  {
                    "fieldLabel": "Recipient email address",
                    "fieldType": "email"
                  },
                  {
                    "fieldLabel": "Recipient name (if known)"
                  }
                ]
              },
              "options": {}
            },
            "id": "42df4827-de81-4c1e-9043-4ece0f1ad715",
            "name": "Sales Email Form",
            "type": "n8n-nodes-base.formTrigger",
            "typeVersion": 2.3,
            "position": [
              784,
              -560
            ]
          },
          {
            "parameters": {
              "mode": "runOnceForEachItem",
              "jsCode": "return {\n  json: {\n    api: \"simple-crawl\",\n    parameters: {\n      url: $input.item.json[\"Target company website\"]\n    }\n  }\n};"
            },
            "type": "n8n-nodes-base.code",
            "typeVersion": 2,
            "position": [
              1024,
              -560
            ],
            "id": "146a90b7-9dc0-428b-9f53-c349ace77740",
            "name": "Build Crawl Request"
          },
          {
            "parameters": {
              "amount": 120
            },
            "type": "n8n-nodes-base.wait",
            "typeVersion": 1.1,
            "position": [
              1456,
              -560
            ],
            "id": "7fa57cdb-a698-4678-945f-c8217b410ecf",
            "name": "Wait"
          },
          {
            "parameters": {
              "method": "POST",
              "url": "https://app.intuned.io/api/v1/workspace/YOUR_WORKSPACE_ID/projects/crawl4ai/run/start",
              "sendHeaders": true,
              "headerParameters": {
                "parameters": [
                  {
                    "name": "x-api-key",
                    "value": "YOUR_API_KEY"
                  }
                ]
              },
              "sendBody": true,
              "bodyParameters": {
                "parameters": [
                  {
                    "name": "api",
                    "value": "={{ $json.api }}"
                  },
                  {
                    "name": "parameters",
                    "value": "={{ $json.parameters }}"
                  }
                ]
              },
              "options": {}
            },
            "type": "n8n-nodes-base.httpRequest",
            "typeVersion": 4.3,
            "position": [
              1248,
              -560
            ],
            "id": "528accaf-0869-42b4-941e-d40c1ba1fe67",
            "name": "Crawl"
          },
          {
            "parameters": {
              "url": "=https://app.intuned.io/api/v1/workspace/YOUR_WORKSPACE_ID/projects/crawl4ai/run/{{ $json.runId }}/result/",
              "sendHeaders": true,
              "headerParameters": {
                "parameters": [
                  {
                    "name": "x-api-key",
                    "value": "YOUR_API_KEY"
                  }
                ]
              },
              "options": {}
            },
            "type": "n8n-nodes-base.httpRequest",
            "typeVersion": 4.3,
            "position": [
              1664,
              -560
            ],
            "id": "7af2b444-fe16-4efb-aa06-f1c55b8fdf10",
            "name": "Get Crawl Result"
          },
          {
            "parameters": {
              "modelId": {
                "__rl": true,
                "value": "chatgpt-4o-latest",
                "mode": "list",
                "cachedResultName": "CHATGPT-4O-LATEST"
              },
              "responses": {
                "values": [
                  {
                    "content": "=You are a sales email expert. Generate a personalized sales email based on the following information:\n\nPRODUCT I'M SELLING:\n{{ $('Sales Email Form').item.json[\"What are you selling?\"] }}\n\nTARGET COMPANY INFO (from their website):\n{{ $('Get Crawl Result').item.json.result.markdown }}\n\nRECIPIENT:\nJob Title: {{ $('Sales Email Form').item.json[\"Job title (e.g., VP Engineering)\"] }}\nName: {{ $('Sales Email Form').item.json[\"Recipient name (if known)\"] || \"there\" }}\n\nINSTRUCTIONS:\n- Write a professional, personalized sales email\n- Reference specific information from their website to show you've done research\n- Keep it under 150 words\n- Include a clear call-to-action\n- Use a friendly but professional tone\n- Output ONLY the email (subject line + body), no additional commentary\n\nFormat:\nSubject: [subject line]\n\n[email body]"
                  }
                ]
              },
              "builtInTools": {},
              "options": {}
            },
            "type": "@n8n/n8n-nodes-langchain.openAi",
            "typeVersion": 2.1,
            "position": [
              1872,
              -560
            ],
            "id": "09914554-a754-4df0-8df7-b095997e8ed7",
            "name": "Message a model",
            "credentials": {
              "openAiApi": {
                "id": "YOUR_OPENAI_CREDENTIAL_ID",
                "name": "OpenAi account"
              }
            }
          },
          {
            "parameters": {
              "mode": "runOnceForEachItem",
              "jsCode": "// Get the AI-generated email\nconst aiResponse = $input.item.json.output[0].content[0].text\n\n// Split into subject and body\nconst lines = aiResponse.trim().split('\\n');\nlet subject = '';\nlet body = '';\n\n// Find subject line\nconst subjectLine = lines.find(line => line.toLowerCase().startsWith('subject:'));\nif (subjectLine) {\n  subject = subjectLine.replace(/^subject:\\s*/i, '').trim();\n  \n  // Get body (everything after subject, skip empty lines)\n  const subjectIndex = lines.indexOf(subjectLine);\n  body = lines.slice(subjectIndex + 1)\n    .filter(line => line.trim().length > 0)\n    .join('\\n')\n    .trim();\n} else {\n  // If no subject found, use first line as subject\n  subject = lines[0];\n  body = lines.slice(1).join('\\n').trim();\n}\n\n// Get form data\nconst formData = $('Sales Email Form').item.json;\n\nreturn {\n  json: {\n    subject: subject,\n    body: body,\n    to: formData[\"Recipient email address\"],\n    from: formData[\"Your email address\"],\n    senderName: formData[\"Your name\"],\n    fullEmail: aiResponse // Keep full email for debugging\n  }\n};"
            },
            "type": "n8n-nodes-base.code",
            "typeVersion": 2,
            "position": [
              2224,
              -560
            ],
            "id": "1469cadf-5e84-41e6-b5c9-4d4117e5f77e",
            "name": "Parse Email"
          },
          {
            "parameters": {
              "sendTo": "={{ $json.to }}",
              "subject": "={{ $json.subject }}",
              "emailType": "text",
              "message": "={{ $json.fullEmail }}",
              "options": {}
            },
            "type": "n8n-nodes-base.gmail",
            "typeVersion": 2.2,
            "position": [
              2448,
              -560
            ],
            "id": "4bf8db92-7bef-4eb6-9870-54920081ef15",
            "name": "Send a message",
            "credentials": {
              "gmailOAuth2": {
                "id": "YOUR_GMAIL_CREDENTIAL_ID",
                "name": "Gmail account"
              }
            }
          }
        ],
        "pinData": {},
        "connections": {
          "Sales Email Form": {
            "main": [
              [
                {
                  "node": "Build Crawl Request",
                  "type": "main",
                  "index": 0
                }
              ]
            ]
          },
          "Build Crawl Request": {
            "main": [
              [
                {
                  "node": "Crawl",
                  "type": "main",
                  "index": 0
                }
              ]
            ]
          },
          "Wait": {
            "main": [
              [
                {
                  "node": "Get Crawl Result",
                  "type": "main",
                  "index": 0
                }
              ]
            ]
          },
          "Crawl": {
            "main": [
              [
                {
                  "node": "Wait",
                  "type": "main",
                  "index": 0
                }
              ]
            ]
          },
          "Get Crawl Result": {
            "main": [
              [
                {
                  "node": "Message a model",
                  "type": "main",
                  "index": 0
                }
              ]
            ]
          },
          "Message a model": {
            "main": [
              [
                {
                  "node": "Parse Email",
                  "type": "main",
                  "index": 0
                }
              ]
            ]
          },
          "Parse Email": {
            "main": [
              [
                {
                  "node": "Send a message",
                  "type": "main",
                  "index": 0
                }
              ]
            ]
          }
        },
        "active": true,
        "settings": {
          "executionOrder": "v1"
        },
        "meta": {
          "templateCredsSetupCompleted": true
        },
        "tags": []
      }
      ```
    </Accordion>
  </Step>

  <Step title="Import into n8n" icon="upload">
    1. Open your n8n instance
    2. Go to **Workflows** in the sidebar and select **Create Workflow**
    3. Select **Import from File** from the top-right menu
    4. Paste the JSON from the accordion above into a file
    5. The workflow will appear on your canvas
  </Step>
</Steps>

### 3. Understand the workflow structure

The workflow consists of 8 nodes that work together to create a complete automation pipeline:

<Frame>
  <img alt="Visual overview of the n8n workflow showing all 8 nodes connected" />
</Frame>

**Data flow:**

1. **Sales Email Form** - Form trigger that collects user input (product details, target company URL, recipient info)
2. **Build Crawl Request** - JavaScript code node that formats the request for Intuned's API
3. **Crawl (HTTP Request)** - Calls Intuned's API to start crawling the target company website
4. **Wait** - Pauses for 120 seconds to allow the crawl to complete (accounts for queue time + execution time)
5. **Get Crawl Result (HTTP Request)** - Fetches the crawled data from Intuned
6. **Message a model (OpenAI)** - Sends the crawled content + form inputs to GPT-4 to generate a personalized email
7. **Parse Email** - JavaScript code node that extracts subject and body from the AI response
8. **Send a message (Gmail)** - Sends the generated email via Gmail

### 4. Configure the Intuned API connection

<Steps>
  <Step title="Update the Crawl node" icon="link">
    Open the **Crawl** HTTP Request node and update the configuration:

    1. **URL**: Replace `YOUR_WORKSPACE_ID` with your actual workspace ID:
       ```
       https://app.intuned.io/api/v1/workspace/{YOUR_WORKSPACE_ID}/projects/crawl4ai/run/start
       ```

    2. **Headers**: Replace `YOUR_API_KEY` with your Intuned API key:
       ```
       x-api-key: YOUR_API_KEY
       ```

    3. **Body Parameters**: These are already configured to pass the crawl request from the previous node

    <Frame>
      <img alt="HTTP Request node configuration showing Intuned API endpoint and headers" />
    </Frame>

    <Tip>
      Store your API key in n8n's credentials manager instead of hardcoding it. Create a new **Header Auth** credential with name `x-api-key` and your API key as the value.
    </Tip>
  </Step>

  <Step title="Update the Get Crawl Result node" icon="link">
    Open the **Get Crawl Result** HTTP Request node and update:

    1. **URL**: Replace `YOUR_WORKSPACE_ID` with your workspace ID:
       ```
       https://app.intuned.io/api/v1/workspace/{YOUR_WORKSPACE_ID}/projects/crawl4ai/run/{{ $json.runId }}/result/
       ```
       Note: `{{ $json.runId }}` is dynamic and comes from the Crawl node response

    2. **Headers**: Use the same API key as the Crawl node
  </Step>

  <Step title="Understanding the Build Crawl Request code" icon="code">
    The **Build Crawl Request** node formats the API request for Intuned. Here's what it does:

    ```javascript theme={null}
    return {
      json: {
        api: "simple-crawl",
        parameters: {
          url: $input.item.json["Target company website"]
        }
      }
    };
    ```

    This code:

    * Sets the API to `"simple-crawl"` (the crawl4ai project's main API)
    * Passes the target URL from the form input
    * Formats it as the JSON body Intuned expects

    You can customize this to use different Intuned APIs or pass additional parameters based on your project.
  </Step>
</Steps>

### 5. Configure OpenAI and Gmail credentials

<Steps>
  <Step title="Set up OpenAI credentials" icon="robot">
    1. Open the **Message a model** node
    2. Select **Create New Credential** under OpenAI API
    3. Enter your OpenAI API key
    4. Save the credential

    See [n8n's OpenAI documentation](https://docs.n8n.io/integrations/builtin/credentials/openai/) for detailed setup instructions.
  </Step>

  <Step title="Set up Gmail credentials" icon="envelope">
    1. Open the **Send a message** node
    2. Select **Create New Credential** under Gmail OAuth2
    3. Follow n8n's OAuth flow to authorize your Gmail account
    4. Save the credential

    See [n8n's Gmail documentation](https://docs.n8n.io/integrations/builtin/credentials/google/) for detailed setup instructions.
  </Step>
</Steps>

### 6. Test the workflow

<Steps>
  <Step title="Run the workflow" icon="play">
    1. Select **Execute workflow** in the bottom to enable the workflow

    2. A UI form on another tab will open automatically

    3. Fill in the form with test data:
       * **What are you selling?**: "Developer productivity platform for automated code reviews and testing"
       * **Target company website**: "[https://github.com](https://github.com)"
       * **Job title**: "VP of Engineering"
       * **Recipient email**: Your test email address
       * Fill in remaining fields

    4. Submit the form and watch the workflow execute

    <Frame>
      <img alt="Workflow executing showing data flowing through each node" />
    </Frame>
  </Step>

  <Step title="Monitor the execution" icon="chart-line">
    In n8n's execution view, you can:

    * See the data output from each node
    * Verify the Crawl node received a `runId`
    * Check that Get Crawl Result returned markdown content
    * Review the AI-generated email in the Parse Email node
    * Confirm the email was sent successfully

    You can also monitor Run status and view logs in the Intuned dashboard. See [Observability and monitoring](/docs/main/02-features/observability-monitoring-logs) for details.
  </Step>

  <Step title="Check the results" icon="check">
    1. Open your test email inbox
    2. You should receive a personalized sales email
    3. The email content should reference information from the target website
    4. Review the subject line and body for quality
  </Step>
</Steps>

## Best practices

* **Use environment variables**: Store API keys and workspace IDs in n8n's credentials manager or environment variables, never hardcode them.
* **Adjust wait times**: The 120-second wait accounts for both queue time and crawl execution. Complex pages or busy projects may need longer. See [Runs](/docs/main/02-features/runs-single-executions) for how concurrency and queuing work.
* **Handle errors**: Add error workflows in n8n to handle API failures, timeout issues, or invalid crawl results gracefully.
* **Monitor in Intuned dashboard**: Check the Intuned dashboard to review Runs, debug failures, and monitor usage. See [Observability and monitoring](/docs/main/02-features/observability-monitoring-logs).
* **Consider webhooks for production**: For long-running crawls, use Intuned's async webhook sinks instead of the Wait node to avoid blocking your workflow.

## Related resources

* **[Intuned Projects](/docs/main/03-how-to/solve/structure-intuned-projects)** — Learn how to structure and manage your Intuned projects
* **[API Reference](/docs/client-apis/api-reference)** — Complete API documentation for all Intuned endpoints
* **[Cookbook examples](https://github.com/Intuned/cookbook)** — Explore more browser automation projects and examples
* **[n8n HTTP Request docs](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest/)** — Official n8n documentation for HTTP Request nodes


# Cloudflare R2
Source: https://intunedhq.com/docs/main/04-integrations/r2



## Overview

By the end of this guide, you'll have an Intuned project (+ scraping Job with R2 sink) that sends scraped data directly to Cloudflare R2. You'll:

1. Create an R2 bucket and configure credentials for Intuned.
2. Configure a Job with an R2 sink.
3. Trigger a Job and verify data lands in R2.

## Prerequisites

Before you begin, ensure you have the following:

* A Cloudflare account with R2 access.
* An Intuned account.

<Note>This guide assumes you have a basic understanding of Intuned Projects and Jobs. If you're new to Intuned, start with the [getting started guide](/docs/main/00-getting-started/introduction).</Note>

## When to use R2 integration

Scrapers built on Intuned typically run via Jobs on a schedule. When a JobRun completes, you want that data sent somewhere for processing or persistence.

R2 integration automatically delivers scraped data to your R2 bucket as JSON files. From there, you can process results using Cloudflare Workers at the edge—or connect to other services. R2's zero egress fees make it ideal for frequent data access.

<Note>While this guide focuses on scraping, R2 integration works for any Intuned Job—the files sent to R2 are Run results from any automation.</Note>

## Guide

### 1. Create an R2 bucket and access credentials

Create an R2 bucket and API credentials that Intuned can use to write data:

<Steps>
  <Step title="Create an R2 bucket" icon="bucket">
    1. Log in to the [Cloudflare Dashboard](https://dash.cloudflare.com/)
    2. Navigate to **Storage & databases > R2 object storage > Overview** from the sidebar
    3. Select **Create bucket**
    4. Enter a bucket name (e.g., `my-intuned-scraper-data`)
    5. Select **Create bucket**

    <Tip>
      R2 buckets are globally accessible with automatic redundancy - no region selection needed unlike AWS S3.
    </Tip>
  </Step>

  <Step title="Get your Cloudflare Account ID" icon="id-card">
    Your Account ID is needed to construct the R2 endpoint URL:

    1. In the Cloudflare dashboard, look at the URL - it contains your Account ID
    2. The URL format is: `https://dash.cloudflare.com/<account-id>/r2`
    3. Copy your Account ID (the alphanumeric string)
    4. Your R2 endpoint will be: `https://<account-id>.r2.cloudflarestorage.com`

    <Info>
      Your Account ID is also visible in the sidebar of the Cloudflare dashboard under your account name.
    </Info>
  </Step>

  <Step title="Generate R2 API token" icon="key">
    Create API credentials for Intuned to access your R2 bucket:

    1. In the **Storage & databases > R2 object storage > Overview** section, select **Manage Tokens**
    2. Select **Create Account API Token**
    3. Configure the token:
       * **Token name**: Enter `intuned-r2-writer`
       * **Permissions**: Select **Object Read & Write**
       * **TTL**: Leave as default or set a custom expiration
       * **Bucket scope**: Select your specific bucket or choose **All buckets**
    4. Select **Create Account API Token**
    5. **Copy both the Access Key ID and Secret Access Key** immediately

    <Warning>
      The Secret Access Key is only shown once and cannot be retrieved later. Save both credentials securely. Never commit credentials to version control.
    </Warning>
  </Step>

  <Step title="Note your configuration details" icon="clipboard">
    You now have everything needed to connect Intuned to R2. Save these details:

    * **Bucket name**: Your R2 bucket name
    * **Account ID**: From the Cloudflare dashboard URL
    * **Endpoint**: `https://<account-id>.r2.cloudflarestorage.com`
    * **Access Key ID**: From the API token
    * **Secret Access Key**: From the API token

    You'll use these in the next section to configure your Intuned Job.
  </Step>
</Steps>

### 2. Configure a Job with an R2 sink

<Steps>
  <Step title="Prepare a project" icon="book-open">
    You can use an existing project or create a new one.

    For this example, we'll use the `ecommerce-scraper-quickstart` project that you can deploy using the [Deploy your first scraper](/docs/main/00-getting-started/quickstarts/scraper) quickstart tutorial.
  </Step>

  <Step title="Create a Job with an R2 sink" icon="cloud">
    <Tabs>
      <Tab title="Dashboard">
        1. Go to [app.intuned.io](https://app.intuned.io)
        2. Open your `ecommerce-scraper-quickstart` project
        3. Select the **Jobs** tab
        4. Select Create Job
        5. Fill in the Job ID and payloads

        * Set Job ID to: `default-with-r2`
        * Set payload api to `list` and empty parameter object `{}`

        6. Enable sink configuration and add your R2 details with the details from the previous section:
           * **Type**: `s3` (R2 uses S3-compatible API)
           * **Bucket**: Your R2 bucket name (e.g., `my-intuned-scraper-data`)
           * **Region**: Set to any value (R2 is zone-agnostic) (e.g., `auto`)
           * **Endpoint**: `https://<your-account-id>.r2.cloudflarestorage.com`
           * **Access Key ID**: Your R2 API token access key
           * **Secret Access Key**: Your R2 API token secret key
           * **Force Path Style**: `true` (required for R2)
           * **Prefix** (optional): A path prefix to organize files (e.g., `ecommerce-data/`)
           * **Skip On Fail** (optional): Check to skip writing failed Runs to R2

        <Frame>
          <img alt="Job Sink Configuration" />
        </Frame>

        7. Select **Save** to create the Job.
      </Tab>

      <Tab title="TypeScript SDK">
        ```typescript theme={null}
        import { IntunedClient } from "@intuned/client";

        const intunedClient = new IntunedClient({
          workspaceId: "your-workspace-id",
          apiKey: process.env["INTUNED_API_KEY"] ?? "",
        });

        async function createJobWithR2Sink() {
          const result = await intunedClient.projects.jobs.create(
            "ecommerce-scraper-quickstart",
            {
              id: "default-with-r2",
              payload: [
                {
                  apiName: "list",
                  parameters: {},
                },
              ],
              configuration: {
                retry: {
                  maximumAttempts: 3,
                },
              },
              sink: {
                type: "s3",
                bucket: "my-intuned-scraper-data",
                endpoint: "https://your-account-id.r2.cloudflarestorage.com",
                accessKeyId: process.env["R2_ACCESS_KEY_ID"] ?? "",
                secretAccessKey: process.env["R2_SECRET_ACCESS_KEY"] ?? "",
                forcePathStyle: true,
                skipOnFail: false,
              },
            }
          );

          console.log(result);
        }

        createJobWithR2Sink();
        ```

        <Tip>
          Store your R2 credentials in environment variables (`R2_ACCESS_KEY_ID` and `R2_SECRET_ACCESS_KEY`) rather than hardcoding them in your source code. The `endpoint` and `forcePathStyle` fields are required for R2.
        </Tip>
      </Tab>

      <Tab title="Python SDK">
        ```python theme={null}
        from intuned_client import IntunedClient
        from intuned_client import models
        import os

        with IntunedClient(
            workspace_id="your-workspace-id",
            api_key=os.getenv("INTUNED_API_KEY", ""),
        ) as ic_client:
            result = ic_client.projects.jobs.create(
                project_name="ecommerce-scraper-quickstart",
                body=models.JobsCreateRequestBody(
                    id="default-with-r2",
                    payload=[
                        {
                            "apiName": "list",
                            "parameters": {},
                        },
                    ],
                    configuration={
                        "retry": {
                            "maximumAttempts": 3,
                        },
                    },
                    sink={
                        "type": "s3",
                        "bucket": "my-intuned-scraper-data",
                        "endpoint": "https://your-account-id.r2.cloudflarestorage.com",
                        "accessKeyId": os.getenv("R2_ACCESS_KEY_ID", ""),
                        "secretAccessKey": os.getenv("R2_SECRET_ACCESS_KEY", ""),
                        "forcePathStyle": True,
                        "skipOnFail": False,
                    },
                ),
            )

            print(result)
        ```

        <Tip>
          Store your R2 credentials in environment variables (`R2_ACCESS_KEY_ID` and `R2_SECRET_ACCESS_KEY`) rather than hardcoding them in your source code. The `endpoint` and `forcePathStyle` fields are required for R2.
        </Tip>
      </Tab>
    </Tabs>
  </Step>

  <Step title="Trigger the Job" icon="play">
    <Tabs>
      <Tab title="Dashboard">
        1. In the Jobs tab, find the Job you created
        2. Select **...** next to the Job
        3. Select **Trigger**

        The Job starts running immediately. You'll see the JobRun appear in the dashboard with status updates.
      </Tab>

      <Tab title="TypeScript SDK">
        ```typescript theme={null}
        import { IntunedClient } from '@intuned/client';

        const intunedClient = new IntunedClient({
          workspaceId: 'your-workspace-id',
          apiKey: process.env.INTUNED_API_KEY ?? ''
        });

        async function triggerJob() {
          const result = await intunedClient.projects.jobs.trigger(
            'ecommerce-scraper-quickstart',
            'default-with-r2'
          );

          console.log(`JobRun started: ${result.id}`);
          console.log(`Monitor at: https://app.intuned.io/workspaces/your-workspace-id/projects/ecommerce-scraper-quickstart/jobs/default-with-r2/${result.id}`);
        }

        triggerJob();
        ```
      </Tab>

      <Tab title="Python SDK">
        ```python theme={null}
        from intuned_client import IntunedClient
        import os

        with IntunedClient(
            workspace_id='your-workspace-id',
            api_key=os.getenv('INTUNED_API_KEY', '')
        ) as ic_client:
            result = ic_client.projects.jobs.trigger(
                project_name='ecommerce-scraper-quickstart',
                job_id='default-with-r2'
            )

            print(f'JobRun started: {result.id}')
            print(f'Monitor at: https://app.intuned.io/workspaces/your-workspace-id/projects/ecommerce-scraper-quickstart/jobs/default-with-r2/{result.id}')
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Inspect data in R2" icon="database">
    After the Job completes, view your scraped data in R2:

    1. Navigate to the [Cloudflare Dashboard](https://dash.cloudflare.com/) → Storage & databases > R2 object storage > Overview
    2. Open your bucket (e.g., `my-intuned-scraper-data`)
    3. Navigate to your prefix path if you specified one (e.g., `ecommerce-data/`)

    **R2 file structure:**

    Files are organized differently depending on whether you're using a Job sink or a Run sink:

    * **Job sink**: `{prefix}/{jobId}/run-{jobRunId}/{apiRunId}.json`
    * **Run sink**: `{prefix}/runs/{apiRunId}.json`

    Since we're using a Job sink in this example, your files follow the Job sink pattern.

    **What to expect:**

    * One JSON file per API Run
    * The initial `list` API Run has one file
    * Each `details` API Run (created by `extendPayload`) has its own file

    <Tip>
      The ecommerce scraper uses `extendPayload` to create detail tasks for each discovered product. You'll see multiple files: one for the initial `list` Run, then one for each `details` Run.
    </Tip>

    **Example R2 payload:**

    <Accordion title="View sample R2 file content">
      ```json theme={null}
      {
        "workspaceId": "e95cb8d1-f212-4c04-ace1-c0f77e8708c7",
        "apiInfo": {
            "name": "details",
            "runId": "656CxOdANRlR5lWUAt_eC",
            "parameters": {
                "detailsUrl": "https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie/",
                "name": "Abominable Hoodie"
            },
            "result": {
                "status": "completed",
                "result": [
                  {
                    "id": "prod-1",
                    "name": "Wireless Headphones",
                    "price": "$79.99"
                  },
                  {
                    "id": "prod-2",
                    "name": "Smart Watch",
                    "price": "$199.99"
                  }
                ],
                "statusCode": 200
            }
        },
        "project": {
            "id": "482bf507-5fcc-43ed-9443-d8fff86015c4",
            "name": "ecommerce-scraper-quickstart"
        },
        "projectJob": {
            "id": "default"
        },
        "projectJobRun": {
            "id": "08523ea6-5c6b-413e-995a-40e4f6fd7846"
        }
      }
      ```
    </Accordion>
  </Step>
</Steps>

After triggering the Job:

1. **Job starts immediately** - Visible in Intuned dashboard
2. **API Runs execute** - The `list` API runs first, then `details` APIs for each product
3. **Files written to R2** - When each API Run completes, Intuned writes a JSON file to your bucket

Check your R2 bucket - you should see the JSON files with the structure documented in the previous step.

<Warning>
  If writing to R2 fails (e.g., due to incorrect credentials, wrong endpoint format, or missing forcePathStyle), Intuned **pauses the Job automatically**. The pause reason is "Failed to write to S3 sink". Verify your R2 endpoint and credentials, fix the issue, and resume the Job from the dashboard.
</Warning>

### Configuration options

For full details on S3-compatible sink configuration, see the [S3 Sink API Reference](/docs/client-apis/api-reference/sinks/s3).

Key configuration fields for R2:

| Field             | Required | Description                                                  |
| ----------------- | -------- | ------------------------------------------------------------ |
| `type`            | Yes      | Must be `"s3"` (R2 uses S3-compatible API)                   |
| `bucket`          | Yes      | R2 bucket name                                               |
| `endpoint`        | Yes      | R2 endpoint: `https://<account_id>.r2.cloudflarestorage.com` |
| `accessKeyId`     | Yes      | R2 API token access key                                      |
| `secretAccessKey` | Yes      | R2 API token secret key                                      |
| `forcePathStyle`  | Yes      | Must be `true` for R2                                        |
| `prefix`          | No       | Path prefix for organizing files                             |
| `skipOnFail`      | No       | Skip writing failed Runs (default: false)                    |
| `apisToSend`      | No       | List of specific API names to send                           |

<Tip>
  R2 doesn't require a `region` field since it's zone-agnostic (globally
  distributed). The `endpoint` and `forcePathStyle` fields are essential for R2
  to work correctly.
</Tip>

## Processing data from R2

Once data lands in R2, process it using **Cloudflare Workers**—a common pattern for serverless data pipelines. Workers run at the edge with zero-latency access to R2 and no egress fees.

Alternatively, use R2 Event Notifications to trigger workflows, or access data via the S3-compatible API from any programming language or tool (AWS CLI, boto3, aws-sdk).

## Best practices

* **Organize data with prefixes**: Use meaningful prefix structures like `{environment}/{project-name}/{date}/` to make data easier to find, manage, and process.
* **Use Cloudflare Workers for processing**: Workers have zero-latency access to R2 data with no egress fees. Use them to transform, filter, or aggregate data at the edge before storing or forwarding.
* **Monitor R2 usage**: Check the R2 dashboard for storage usage and request counts. Set up alerts for storage thresholds and track API request patterns.

## Troubleshooting

### Endpoint connection errors

**Cause:** Incorrect Account ID in endpoint URL, missing or malformed endpoint field, or typo in URL format.

**Solution:** Find your Account ID in the Cloudflare dashboard URL (`https://dash.cloudflare.com/<account-id>/r2`). Verify endpoint format is exactly `https://<account-id>.r2.cloudflarestorage.com` with no trailing slash or extra paths.

### forcePathStyle errors

**Cause:** Missing or incorrect `forcePathStyle` setting. R2 requires path-style URLs, not the virtual-hosted-style URLs that AWS S3 uses by default.

**Solution:** Ensure `forcePathStyle` is set to `true` in your sink configuration. This field is required for R2 to work correctly.

### Job paused: "Failed to write to S3 sink"

**Cause:** Intuned automatically pauses the Job when it fails to write data to R2. Common reasons include invalid or expired API token, incorrect R2 endpoint format, missing `forcePathStyle: true` setting, insufficient token permissions, or the bucket doesn't exist.

**Solution:** Check the Job status in the Intuned dashboard (shows as "Paused"). Fix the underlying issue by verifying R2 credentials, confirming endpoint format, ensuring `forcePathStyle` is `true`, and checking token permissions include Object Read & Write. Test credentials with `aws s3 ls s3://your-bucket --endpoint-url=https://your-account-id.r2.cloudflarestorage.com`. Update the Job configuration if needed, then select **Resume** from the dashboard. The Job continues from where it paused.

## Related resources

<CardGroup>
  <Card title="S3 Sink API Reference" icon="code" href="/client-apis/api-reference/sinks/s3">
    S3-compatible API documentation that works with R2
  </Card>

  <Card title="Jobs" icon="play" href="/main/02-features/jobs-batched-executions">
    Learn more about creating and managing batched Job executions
  </Card>

  <Card title="Deploy your first scraper" icon="rocket" href="/main/00-getting-started/quickstarts/scraper">
    Complete the quickstart tutorial to set up your first scraping project
  </Card>

  <Card title="AWS S3 Integration" icon="database" href="/main/04-integrations/s3/s3-job-sink">
    Compare R2 with AWS S3 integration and setup differences
  </Card>
</CardGroup>


# S3 Attachment Storage
Source: https://intunedhq.com/docs/main/04-integrations/s3/s3-attachment-storage



## Overview

By the end of this guide, you'll have an Intuned project that saves downloaded files directly to AWS S3. You'll:

1. Create an S3 bucket and configure AWS credentials for Intuned.
2. Configure S3 credentials in your project.
3. Use the Browser SDK to save files to S3.

## Prerequisites

Before you begin, ensure you have the following:

* An AWS account with S3 access.
* An Intuned account.

<Note>This guide assumes you have a basic understanding of Intuned Projects. If you're new to Intuned, start with the [getting started guide](/docs/main/00-getting-started/introduction).</Note>

## When to use S3 attachment storage

Browser automations often need to download files—invoices from portals, reports from dashboards, or documents from authenticated sites. Instead of returning file content directly from your API, you can save files to S3 for better control and longer term access.

## Guide

### 1. Create an S3 bucket and access credentials

Create an S3 bucket and IAM credentials that Intuned can use to write data:

<Steps>
  <Step title="Create an S3 bucket" icon="bucket">
    1. Log in to the [AWS Management Console](https://console.aws.amazon.com/)
    2. Navigate to the S3 service
    3. Select **Create bucket**
    4. Enter a unique bucket name (e.g., `my-intuned-data`)

    <Tip>
      Choose a descriptive bucket name that makes it easy to identify its purpose (e.g., `company-intuned-production`).
    </Tip>
  </Step>

  <Step title="Configure bucket settings" icon="gear">
    When creating your bucket:

    1. **Object Ownership**: Set to "Access Control Lists (ACLs) disabled"
    2. **Block Public Access**: Keep all public access blocked (recommended for security)
    3. **Bucket Versioning**: Optional - enable if you want to keep historical versions of files
    4. **Encryption**: Optional - enable default encryption for data at rest
    5. Select **Create bucket** to finish

    <Info>
      Intuned only needs write access to your bucket, so keeping public access blocked is safe and recommended.
    </Info>
  </Step>

  <Step title="Create an IAM user for Intuned" icon="user">
    Create a dedicated IAM user with limited permissions for Intuned:

    1. Navigate to **IAM** in the AWS Console
    2. Select **Users** in the left sidebar, then **Create user**
    3. Enter a username (e.g., `intuned-s3-writer`)
    4. Select **Next**, which takes you to the permissions page

    On the permissions page:

    1. Select **Attach existing policies directly**
    2. Select **Create policy** (opens in new tab)
    3. Select the **JSON** tab and paste this policy:

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": [
            "s3:PutObject"
          ],
          "Resource": "arn:aws:s3:::YOUR-BUCKET-NAME/*"
        }
      ]
    }
    ```

    4. Replace `YOUR-BUCKET-NAME` with your actual bucket name
    5. Select **Next**, which takes you to the Review page
    6. Name the policy `IntunedS3WritePolicy`
    7. Select **Create policy**

    <Warning>
      Replace `YOUR-BUCKET-NAME` in the policy with your actual bucket name. Don't use root account credentials - always create a dedicated IAM user.
    </Warning>
  </Step>

  <Step title="Attach policy and generate access keys" icon="key">
    Back in the user creation flow:

    1. Refresh the policies list
    2. Search for `IntunedS3WritePolicy`
    3. Select the checkbox next to the policy
    4. Select **Next** to go to the Review page
    5. Select **Create user**

    Then open the newly created user page:

    1. Go to the **Security credentials** tab
    2. Select **Create access key**
    3. Choose **Application running outside AWS** and select **Next**
    4. Select **Create access key**
    5. **Copy the Access key ID** - you'll need this for Intuned
    6. **Copy the Secret access key** - you'll need this for Intuned (only shown once)
    7. Download the CSV or save these credentials securely

    <Warning>
      Store your credentials securely. The secret access key is only shown once and cannot be retrieved later. Never commit credentials to version control.
    </Warning>
  </Step>

  <Step title="Note your configuration details" icon="clipboard">
    You now have everything needed to configure S3 in Intuned. Save these details:

    * **Bucket name**: Your S3 bucket name
    * **Region**: Your AWS region (e.g., `us-west-2`)
    * **Access key ID**: From the IAM user
    * **Secret access key**: From the IAM user
  </Step>
</Steps>

You'll use these credentials in your automation code.

### 2. Configure the S3 bucket as your attachment store

<Info>
  This guide is coming soon. In the meantime, you can use the `saveFileToS3` helper from the Browser SDK to save files to S3.

  See the [saveFileToS3 documentation](/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/saveFileToS3) for usage details.
</Info>

## Related resources

<CardGroup>
  <Card title="saveFileToS3" icon="code" href="/automation-sdks/intuned-sdk/typescript/helpers/functions/saveFileToS3">
    Browser SDK helper for saving files to S3
  </Card>

  <Card title="downloadFile" icon="download" href="/automation-sdks/intuned-sdk/typescript/helpers/functions/downloadFile">
    Browser SDK helper for downloading files
  </Card>
</CardGroup>


# S3 Job Sink
Source: https://intunedhq.com/docs/main/04-integrations/s3/s3-job-sink



## Overview

By the end of this guide, you'll have an Intuned project (+ scraping Job with S3 sink) that sends scraped data directly to AWS S3. You'll:

1. Create an S3 bucket and configure AWS credentials for Intuned.
2. Configure a Job with an S3 sink.
3. Trigger a Job and verify data lands in S3.

## Prerequisites

Before you begin, ensure you have the following:

* An AWS account with S3 access.
* An Intuned account.

<Note>This guide assumes you have a basic understanding of Intuned Projects and Jobs. If you're new to Intuned, start with the [getting started guide](/docs/main/00-getting-started/introduction).</Note>

## When to use S3 integration

Scrapers built on Intuned typically run via Jobs on a schedule. When a JobRun completes, you want that data sent somewhere for processing or persistence.

S3 integration automatically delivers scraped data to your S3 bucket as JSON files. From there, you can process results using AWS tools like Lambda—or connect to other services.

<Note>While this guide focuses on scraping, S3 integration works for any Intuned Job—the files sent to S3 are Run results from any automation.</Note>

## Guide

### 1. Create an S3 bucket and access credentials

Create an S3 bucket and IAM credentials that Intuned can use to write data:

<Steps>
  <Step title="Create an S3 bucket" icon="bucket">
    1. Log in to the [AWS Management Console](https://console.aws.amazon.com/)
    2. Navigate to the S3 service
    3. Select **Create bucket**
    4. Enter a unique bucket name (e.g., `my-intuned-data`)

    <Tip>
      Choose a descriptive bucket name that makes it easy to identify its purpose (e.g., `company-intuned-production`).
    </Tip>
  </Step>

  <Step title="Configure bucket settings" icon="gear">
    When creating your bucket:

    1. **Object Ownership**: Set to "Access Control Lists (ACLs) disabled"
    2. **Block Public Access**: Keep all public access blocked (recommended for security)
    3. **Bucket Versioning**: Optional - enable if you want to keep historical versions of files
    4. **Encryption**: Optional - enable default encryption for data at rest
    5. Select **Create bucket** to finish

    <Info>
      Intuned only needs write access to your bucket, so keeping public access blocked is safe and recommended.
    </Info>
  </Step>

  <Step title="Create an IAM user for Intuned" icon="user">
    Create a dedicated IAM user with limited permissions for Intuned:

    1. Navigate to **IAM** in the AWS Console
    2. Select **Users** in the left sidebar, then **Create user**
    3. Enter a username (e.g., `intuned-s3-writer`)
    4. Select **Next**, which takes you to the permissions page

    On the permissions page:

    1. Select **Attach existing policies directly**
    2. Select **Create policy** (opens in new tab)
    3. Select the **JSON** tab and paste this policy:

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": [
            "s3:PutObject"
          ],
          "Resource": "arn:aws:s3:::YOUR-BUCKET-NAME/*"
        }
      ]
    }
    ```

    4. Replace `YOUR-BUCKET-NAME` with your actual bucket name
    5. Select **Next**, which takes you to the Review page
    6. Name the policy `IntunedS3WritePolicy`
    7. Select **Create policy**

    <Warning>
      Replace `YOUR-BUCKET-NAME` in the policy with your actual bucket name. Don't use root account credentials - always create a dedicated IAM user.
    </Warning>
  </Step>

  <Step title="Attach policy and generate access keys" icon="key">
    Back in the user creation flow:

    1. Refresh the policies list
    2. Search for `IntunedS3WritePolicy`
    3. Select the checkbox next to the policy
    4. Select **Next** to go to the Review page
    5. Select **Create user**

    Then open the newly created user page:

    1. Go to the **Security credentials** tab
    2. Select **Create access key**
    3. Choose **Application running outside AWS** and select **Next**
    4. Select **Create access key**
    5. **Copy the Access key ID** - you'll need this for Intuned
    6. **Copy the Secret access key** - you'll need this for Intuned (only shown once)
    7. Download the CSV or save these credentials securely

    <Warning>
      Store your credentials securely. The secret access key is only shown once and cannot be retrieved later. Never commit credentials to version control.
    </Warning>
  </Step>

  <Step title="Note your configuration details" icon="clipboard">
    You now have everything needed to configure S3 in Intuned. Save these details:

    * **Bucket name**: Your S3 bucket name
    * **Region**: Your AWS region (e.g., `us-west-2`)
    * **Access key ID**: From the IAM user
    * **Secret access key**: From the IAM user
  </Step>
</Steps>

You'll use these in the next section to configure your Intuned Job.

### 2. Configure a Job with an S3 sink

Now that your S3 bucket is ready, add an S3 sink to a Job so Run results are delivered to your bucket.

<Steps>
  <Step title="Prepare a project" icon="book-open">
    You can use an existing project or create a new one.

    For this example, we'll use the `ecommerce-scraper-quickstart` project that you can deploy using the [Deploy your first scraper](/docs/main/00-getting-started/quickstarts/scraper) quickstart tutorial.
  </Step>

  <Step title="Create a Job with S3 sink" icon="cloud">
    <Tabs>
      <Tab title="Dashboard">
        1. Go to [app.intuned.io](https://app.intuned.io)
        2. Open your `ecommerce-scraper-quickstart` project
        3. Select the **Jobs** tab
        4. Select **Create Job**
        5. Fill in the Job details:
           * **Job ID**: `default-with-s3`
           * **Payload API**: `list`
           * **Payload Parameters**: `{}`
        6. Enable sink configuration and add your S3 details:
           * **Type**: `s3`
           * **Bucket**: Your S3 bucket name (e.g., `my-intuned-scraper-data`)
           * **Region**: Your AWS region (e.g., `us-west-2`)
           * **Access Key ID**: Your IAM user access key
           * **Secret Access Key**: Your IAM user secret key
           * **Prefix** (optional): A path prefix to organize files (e.g., `ecommerce-data/`)
           * **Skip On Fail** (optional): Check to skip writing failed Runs to S3

        <Frame>
          <img alt="Job Sink Configuration" />
        </Frame>

        7. Select **Save** to create the Job.
      </Tab>

      <Tab title="TypeScript SDK">
        ```typescript theme={null}
        import { IntunedClient } from '@intuned/client';

        const intunedClient = new IntunedClient({
          workspaceId: 'your-workspace-id',
          apiKey: process.env.INTUNED_API_KEY ?? ''
        });

        async function createJobWithS3Sink() {
          const result = await intunedClient.projects.jobs.create(
            'ecommerce-scraper-quickstart',
            {
              id: 'default-with-s3',
              payload: [
                {
                  apiName: 'list',
                  parameters: {}
                }
              ],
              configuration: {
                retry: {
                  maximumAttempts: 3
                }
              },
              sink: {
                type: 's3',
                bucket: 'my-intuned-scraper-data',
                region: 'us-west-2',
                accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
                secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
                prefix: 'ecommerce-data/',
                skipOnFail: false
              }
            }
          );

          console.log('Job created with S3 sink:', result.id);
        }

        createJobWithS3Sink();
        ```

        <Tip>
          Store your AWS credentials in environment variables (`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`) rather than hardcoding them in your source code.
        </Tip>
      </Tab>

      <Tab title="Python SDK">
        ```python theme={null}
        from intuned_client import IntunedClient
        from intuned_client import models
        import os

        with IntunedClient(
            workspace_id='your-workspace-id',
            api_key=os.getenv('INTUNED_API_KEY', '')
        ) as ic_client:
            result = ic_client.projects.jobs.create(
                project_name='ecommerce-scraper-quickstart',
                body=models.JobsCreateRequestBody(
                    id='default-with-s3',
                    payload=[
                        {
                            'apiName': 'list',
                            'parameters': {}
                        }
                    ],
                    configuration={
                        'retry': {
                            'maximumAttempts': 3
                        }
                    },
                    sink={
                        'type': 's3',
                        'bucket': 'my-intuned-scraper-data',
                        'region': 'us-west-2',
                        'accessKeyId': os.getenv('AWS_ACCESS_KEY_ID'),
                        'secretAccessKey': os.getenv('AWS_SECRET_ACCESS_KEY'),
                        'prefix': 'ecommerce-data/',
                        'skipOnFail': False
                    }
                )
            )

            print(f'Job created with S3 sink: {result.id}')
        ```

        <Tip>
          Store your AWS credentials in environment variables (`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`) rather than hardcoding them in your source code.
        </Tip>
      </Tab>
    </Tabs>
  </Step>

  <Step title="Trigger the Job" icon="play">
    <Tabs>
      <Tab title="Dashboard">
        1. In the Jobs tab, find your new Job (`default-with-s3`)
        2. Select **...** next to the Job
        3. Select **Trigger**

        The Job starts running immediately. You'll see the JobRun appear in the dashboard with status updates.
      </Tab>

      <Tab title="TypeScript SDK">
        ```typescript theme={null}
        import { IntunedClient } from '@intuned/client';

        const intunedClient = new IntunedClient({
          workspaceId: 'your-workspace-id',
          apiKey: process.env.INTUNED_API_KEY ?? ''
        });

        async function triggerJob() {
          const result = await intunedClient.projects.jobs.trigger(
            'ecommerce-scraper-quickstart',
            'default-with-s3'
          );

          console.log(`JobRun started: ${result.id}`);
        }

        triggerJob();
        ```
      </Tab>

      <Tab title="Python SDK">
        ```python theme={null}
        from intuned_client import IntunedClient
        import os

        with IntunedClient(
            workspace_id='your-workspace-id',
            api_key=os.getenv('INTUNED_API_KEY', '')
        ) as ic_client:
            result = ic_client.projects.jobs.trigger(
                project_name='ecommerce-scraper-quickstart',
                job_id='default-with-s3'
            )

            print(f'JobRun started: {result.id}')
        ```
      </Tab>
    </Tabs>

    After triggering:

    1. **JobRun starts immediately** - Visible in the Intuned dashboard
    2. **API Runs execute** - The `list` API runs first, then `details` APIs for each product
    3. **Files written to S3** - When each API Run completes, Intuned writes a JSON file to your bucket
  </Step>

  <Step title="Inspect data in S3" icon="database">
    After the Job completes, view your data in S3:

    1. Navigate to the [S3 Console](https://console.aws.amazon.com/s3)
    2. Open your bucket (e.g., `my-intuned-scraper-data`)
    3. Navigate to your prefix path if you specified one (e.g., `ecommerce-data/`)

    **S3 file structure:**

    Files are organized differently depending on whether you're using a Job sink or a Run sink:

    * **Job sink**: `{prefix}/{jobId}/run-{jobRunId}/{apiRunId}.json`
    * **Run sink**: `{prefix}/runs/{apiRunId}.json`

    Since we're using a Job sink in this example, your files follow the Job sink pattern.

    **What to expect:**

    * One JSON file per API Run
    * The initial `list` API Run has one file
    * Each `details` API Run (created by `extendPayload`) has its own file

    <Tip>
      The ecommerce scraper uses `extendPayload` to create detail tasks for each discovered product. You'll see multiple files: one for the initial `list` Run, then one for each `details` Run.
    </Tip>

    **Example S3 payload:**

    <Accordion title="View sample S3 file content">
      ```json theme={null}
      {
        "workspaceId": "e95cb8d1-f212-4c04-ace1-c0f77e8708c7",
        "apiInfo": {
            "name": "details",
            "runId": "656CxOdANRlR5lWUAt_eC",
            "parameters": {
                "detailsUrl": "https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie/",
                "name": "Abominable Hoodie"
            },
            "result": {
                "status": "completed",
                "result": [
                  {
                    "id": "prod-1",
                    "name": "Wireless Headphones",
                    "price": "$79.99"
                  },
                  {
                    "id": "prod-2",
                    "name": "Smart Watch",
                    "price": "$199.99"
                  }
                ],
                "statusCode": 200
            }
        },
        "project": {
            "id": "482bf507-5fcc-43ed-9443-d8fff86015c4",
            "name": "ecommerce-scraper-quickstart"
        },
        "projectJob": {
            "id": "default"
        },
        "projectJobRun": {
            "id": "08523ea6-5c6b-413e-995a-40e4f6fd7846"
        }
      }
      ```
    </Accordion>
  </Step>
</Steps>

<Warning>
  If writing to S3 fails (e.g., due to incorrect credentials or insufficient permissions), Intuned **pauses the Job automatically**. The pause reason is "Failed to write to S3 sink". Check your credentials, fix the issue, and resume the Job from the dashboard.
</Warning>

### Configuration options

For full details on S3 sink configuration and available options, see the [S3 Sink API Reference](/docs/client-apis/api-reference/sinks/s3).

Key configuration fields:

| Field             | Required | Description                                            |
| ----------------- | -------- | ------------------------------------------------------ |
| `bucket`          | Yes      | S3 bucket name                                         |
| `region`          | Yes      | AWS region (e.g., `us-west-2`)                         |
| `accessKeyId`     | Yes      | AWS access key ID                                      |
| `secretAccessKey` | Yes      | AWS secret access key                                  |
| `prefix`          | No       | Path prefix for organizing files                       |
| `skipOnFail`      | No       | Skip writing failed Runs to S3 (default: false)        |
| `apisToSend`      | No       | List of specific API names to send (default: all APIs) |
| `endpoint`        | No       | Custom endpoint for S3-compatible services             |
| `forcePathStyle`  | No       | Use path-style URLs for S3-compatible services         |

## Processing data from S3

Once data lands in S3, you can process it in various ways depending on your needs.

A common pattern is using an **AWS Lambda** that triggers automatically when a new file arrives. Typical processing steps include:

* Normalizing the data structure
* Removing empty fields
* Validating against a schema
* Persisting to a database or data warehouse

Every company has different requirements—some use Athena for querying, others pipe data to Snowflake or BigQuery. Choose the approach that fits your data pipeline.

## Best practices

* **Use least privilege IAM policies**: Create a dedicated IAM user for Intuned with only `s3:PutObject` permission. Restrict access to specific bucket paths using resource ARNs. Never use root account credentials.
* **Organize data with prefixes**: Use meaningful prefix structures like `{environment}/{project-name}/{date}/` to make data easier to find, manage, and set lifecycle policies on.
* **Set up lifecycle policies**: Reduce storage costs by transitioning older data to S3 Glacier and deleting data you no longer need. This can reduce costs significantly for infrequently accessed data.
* **Monitor usage and costs**: Enable S3 Storage Lens for bucket-level insights, set up CloudWatch alarms for unexpected growth, and use Cost Explorer to track costs by bucket.

## Troubleshooting

### Job paused: "Failed to write to S3 sink"

**Cause:** Intuned automatically pauses the Job when it fails to write data to S3. Common reasons include invalid or expired AWS credentials, insufficient IAM permissions (missing `s3:PutObject`), incorrect bucket name or region, or the bucket doesn't exist.

**Solution:** Check the Job status in the Intuned dashboard (shows as "Paused"). Fix the underlying issue by verifying AWS credentials, ensuring IAM policy includes `s3:PutObject` permission, and confirming bucket name and region match your configuration. Test credentials with `aws s3 ls s3://your-bucket-name`. Update the Job configuration if needed, then select **Resume** from the dashboard. The Job continues from where it paused.

## Related resources

<CardGroup>
  <Card title="S3 Sink API Reference" icon="code" href="/client-apis/api-reference/sinks/s3">
    Complete API documentation for S3 sink configuration and options
  </Card>

  <Card title="Jobs" icon="play" href="/main/02-features/jobs-batched-executions">
    Learn more about creating and managing batched Job executions
  </Card>

  <Card title="Runs (Single API executions)" icon="rocket" href="/main/02-features/runs-single-executions">
    Learn about running single API executions outside of Jobs
  </Card>

  <Card title="Monitoring and traces" icon="eye" href="/main/02-features/observability-monitoring-logs">
    Debug and monitor your automation runs with traces and logs
  </Card>
</CardGroup>


# S3 standalone Run sink
Source: https://intunedhq.com/docs/main/04-integrations/s3/s3-runs-sink



## Overview

By the end of this guide, you'll have an Intuned project that triggers an RPA automation (via standalone Runs) and sinks its results to S3. You'll:

1. Create an S3 bucket and configure AWS credentials for Intuned.
2. Trigger a Standalone Run with an S3 sink.

## Prerequisites

You'll need:

* An AWS account with S3 access.
* An Intuned account.

<Note>This guide assumes familiarity with Intuned Projects and standalone Runs. If you're new to Intuned, start with the [getting started guide](/docs/main/00-getting-started/introduction).</Note>

## When to use S3 integration with Standalone Runs

Standalone Runs expose a start and result API for executing single API calls on demand. To receive results or check on the Run status, you poll the result endpoint.

S3 integration automatically delivers the results to your S3 bucket as a JSON file. From there, you can process results using AWS tools like Lambda—or connect to other services.

## Guide

### 1. Create an S3 bucket and access credentials

Create an S3 bucket and IAM credentials that Intuned can use to write data:

<Steps>
  <Step title="Create an S3 bucket" icon="bucket">
    1. Log in to the [AWS Management Console](https://console.aws.amazon.com/)
    2. Navigate to the S3 service
    3. Select **Create bucket**
    4. Enter a unique bucket name (e.g., `my-intuned-data`)

    <Tip>
      Choose a descriptive bucket name that makes it easy to identify its purpose (e.g., `company-intuned-production`).
    </Tip>
  </Step>

  <Step title="Configure bucket settings" icon="gear">
    When creating your bucket:

    1. **Object Ownership**: Set to "Access Control Lists (ACLs) disabled"
    2. **Block Public Access**: Keep all public access blocked (recommended for security)
    3. **Bucket Versioning**: Optional - enable if you want to keep historical versions of files
    4. **Encryption**: Optional - enable default encryption for data at rest
    5. Select **Create bucket** to finish

    <Info>
      Intuned only needs write access to your bucket, so keeping public access blocked is safe and recommended.
    </Info>
  </Step>

  <Step title="Create an IAM user for Intuned" icon="user">
    Create a dedicated IAM user with limited permissions for Intuned:

    1. Navigate to **IAM** in the AWS Console
    2. Select **Users** in the left sidebar, then **Create user**
    3. Enter a username (e.g., `intuned-s3-writer`)
    4. Select **Next**, which takes you to the permissions page

    On the permissions page:

    1. Select **Attach existing policies directly**
    2. Select **Create policy** (opens in new tab)
    3. Select the **JSON** tab and paste this policy:

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": [
            "s3:PutObject"
          ],
          "Resource": "arn:aws:s3:::YOUR-BUCKET-NAME/*"
        }
      ]
    }
    ```

    4. Replace `YOUR-BUCKET-NAME` with your actual bucket name
    5. Select **Next**, which takes you to the Review page
    6. Name the policy `IntunedS3WritePolicy`
    7. Select **Create policy**

    <Warning>
      Replace `YOUR-BUCKET-NAME` in the policy with your actual bucket name. Don't use root account credentials - always create a dedicated IAM user.
    </Warning>
  </Step>

  <Step title="Attach policy and generate access keys" icon="key">
    Back in the user creation flow:

    1. Refresh the policies list
    2. Search for `IntunedS3WritePolicy`
    3. Select the checkbox next to the policy
    4. Select **Next** to go to the Review page
    5. Select **Create user**

    Then open the newly created user page:

    1. Go to the **Security credentials** tab
    2. Select **Create access key**
    3. Choose **Application running outside AWS** and select **Next**
    4. Select **Create access key**
    5. **Copy the Access key ID** - you'll need this for Intuned
    6. **Copy the Secret access key** - you'll need this for Intuned (only shown once)
    7. Download the CSV or save these credentials securely

    <Warning>
      Store your credentials securely. The secret access key is only shown once and cannot be retrieved later. Never commit credentials to version control.
    </Warning>
  </Step>

  <Step title="Note your configuration details" icon="clipboard">
    You now have everything needed to configure S3 in Intuned. Save these details:

    * **Bucket name**: Your S3 bucket name
    * **Region**: Your AWS region (e.g., `us-west-2`)
    * **Access key ID**: From the IAM user
    * **Secret access key**: From the IAM user
  </Step>
</Steps>

You'll use these in the next section to trigger your Run.

### 2. Trigger a Run with an S3 sink

Now that your S3 bucket is ready, add an S3 sink to a Run so results are delivered to your bucket.

#### Prepare a Project

You can use an existing Project or create a new one.

For this example, we'll use the `book-consultations-quickstart` project that you can deploy using the [Deploy your first RPA](/docs/main/00-getting-started/quickstarts/rpa) quickstart tutorial.

#### Trigger a Run with S3 sink

<Tabs>
  <Tab title="Dashboard">
    1. Go to [app.intuned.io](https://app.intuned.io)
    2. Open your `book-consultations-quickstart` project
    3. Select the **Runs** tab
    4. Select **Start Run**
    5. Fill in the Run details:
       * **API**: `book-consultations`
       * **Parameters**:

    ```json theme={null}
    {
      "name": "Jane Smith",
      "email": "jane.smith@example.com",
      "phone": "+1(555)123-4567",
      "date": "2025-12-10",
      "time": "14:30",
      "topic": "web-scraping"
    }
    ```

    6. Enable sink configuration and add your S3 details:
       * **Type**: `s3`
       * **Bucket**: Your S3 bucket name (e.g., `my-intuned-data`)
       * **Region**: Your AWS region (e.g., `us-west-2`)
       * **Access Key ID**: Your IAM user access key
       * **Secret Access Key**: Your IAM user secret key
       * **Prefix** (optional): A path prefix to organize files (e.g., `book-consultations-data/`)
       * **Skip On Fail** (optional): Check to skip writing if the Run fails.

    <Frame>
      <img alt="Standalone Run sink configuration" />
    </Frame>

    7. Select **Start Run**
  </Tab>

  <Tab title="API">
    <CodeGroup>
      ```typescript TypeScript theme={null}
      await intuned.projects.runs.start("book-consultations-quickstart", {
        api: "book-consultations",
        parameters: {
          name: "Jane Smith",
          email: "jane.smith@example.com",
          phone: "+1(555)123-4567",
          date: "2025-12-10",
          time: "14:30",
          topic: "web-scraping"
        },
        sink: {
          type: "s3",
          region: "us-west-2",
          bucket: "my-intuned-data",
          accessKeyId: "<ACCESS-KEY-ID>",
          secretAccessKey: "<SECRET-ACCESS-KEY>"
        }
      });
      ```

      ```python Python theme={null}
      from intuned_client import models

      result = intuned.projects.runs.start(
          project_name="book-consultations-quickstart",
          body=models.RunStartRequestBody(
            api="book-consultations",
            parameters={
              "name": "Jane Smith",
              "email": "jane.smith@example.com",
              "phone": "+1(555)123-4567",
              "date": "2025-12-10",
              "time": "14:30",
              "topic": "web-scraping"
            },
            sink={
              "type": "s3",
              "region": "us-west-2",
              "bucket": "my-intuned-data",
              "accessKeyId": "<ACCESS-KEY-ID>",
              "secretAccessKey": "<SECRET-ACCESS-KEY>"
            }
          )
      )
      ```
    </CodeGroup>
  </Tab>
</Tabs>

#### Inspect data in S3

After the Run completes, view your data in S3:

1. Navigate to the [S3 Console](https://console.aws.amazon.com/s3)
2. Open your bucket (e.g., `my-intuned-data`)
3. Navigate to `{prefix}/runs/{runId}.json` and examine the file.

```json theme={null}
{
  "workspaceId": "<workspace-id>",
  "apiInfo": {
    "name": "book-consultations",
    "runId": "<run-id>",
    "parameters": {
      "name": "Jane Smith",
      "email": "jane.smith@example.com",
      "phone": "+1(555)123-4567",
      "date": "2025-12-10",
      "time": "14:30",
      "topic": "web-scraping"
    },
    "result": { 
      "status": "completed",
      "result": {
        "success": true,
        "date": "2025-12-10",
        "message": "Consultation successfully booked for 2025-12-10 at 14:30"
      },
      "statusCode": 200 
    }
  },
  "project": {
    "id": "<project-id>",
    "name": "book-consultations-quickstart"
  }
}

```

### Configuration options

For full details on S3 sink configuration and available options, see the [S3 Sink API Reference](/docs/client-apis/api-reference/sinks/s3).

Key configuration fields:

| Field             | Required | Description                                     |
| ----------------- | -------- | ----------------------------------------------- |
| `bucket`          | Yes      | S3 bucket name                                  |
| `region`          | Yes      | AWS region (e.g., `us-west-2`)                  |
| `accessKeyId`     | Yes      | AWS access key ID                               |
| `secretAccessKey` | Yes      | AWS secret access key                           |
| `prefix`          | No       | Path prefix for organizing files                |
| `skipOnFail`      | No       | Skip writing failed Runs to S3 (default: false) |
| `endpoint`        | No       | Custom endpoint for S3-compatible services      |
| `forcePathStyle`  | No       | Use path-style URLs for S3-compatible services  |

## Processing data from S3

Once data lands in S3, you can process it in various ways depending on your needs.

A common pattern is using an AWS Lambda function that triggers automatically when a new file arrives. Typical processing tasks include:

* Normalizing the data structure
* Removing empty fields
* Validating against a schema
* Triggering workflows such as sending emails, updating billing systems, or invoking other services

## Best practices

* **Use least privilege IAM policies**: Create a dedicated IAM user for Intuned with only `s3:PutObject` permission. Restrict access to specific bucket paths using resource ARNs. Never use root account credentials.
* **Organize data with prefixes**: Use meaningful prefix structures like `{environment}/{project-name}/{date}/` to make data easier to find, manage, and set lifecycle policies on.
* **Set up lifecycle policies**: Reduce storage costs by transitioning older data to S3 Glacier and deleting data you no longer need. This can reduce costs significantly for infrequently accessed data.
* **Monitor usage and costs**: Enable S3 Storage Lens for bucket-level insights, set up CloudWatch alarms for unexpected growth, and use Cost Explorer to track costs by bucket.

## Related resources

<CardGroup>
  <Card title="S3 Sink API Reference" icon="code" href="/client-apis/api-reference/sinks/s3">
    Complete API documentation for S3 sink configuration and options
  </Card>

  <Card title="Runs (Single API executions)" icon="rocket" href="/main/02-features/runs-single-executions">
    Learn more about running single APIs
  </Card>

  <Card title="Jobs" icon="play" href="/main/02-features/jobs-batched-executions">
    Learn about running batched API executions with Jobs
  </Card>

  <Card title="Monitoring and traces" icon="eye" href="/main/02-features/observability-monitoring-logs">
    Debug and monitor your automation runs with traces and logs
  </Card>
</CardGroup>


# Stagehand
Source: https://intunedhq.com/docs/main/04-integrations/stagehand



## Overview

[Stagehand](https://www.stagehand.dev/) is an AI-powered browser automation framework with methods like `observe()`, `extract()`, and `act()`.

This guide walks you through deploying and running a Stagehand project on Intuned. You'll build a sample scraper that extracts book data—without writing Playwright selectors or custom parsing logic. The same patterns apply to any automation you build with the framework.

<Note>This guide assumes you have a basic understanding of Intuned projects. If you're new to Intuned, start with the [getting started guide](/docs/main/00-getting-started/introduction).</Note>

## When to use AI automation

Intuned supports AI-powered browser automation frameworks like Stagehand, Browser Use, and others. Use AI automation when:

* **Pages are dynamic** — Elements change position, structure, or content unpredictably
* **You don't know the exact page structure** — Scraping sites you haven't mapped in detail
* **You want natural language control** — Describe what to do instead of writing precise selectors
* **Traditional Playwright code is too brittle for your case** — AI agents adapt to minor UI changes automatically

Stagehand is one option for AI automation on Intuned. The setup patterns in this guide apply to other AI frameworks as well.

For a deeper dive into choosing between deterministic, AI-driven, and hybrid approaches, check out [Flexible Automations](/docs/main/02-features/flexible-automation).

## Guide

Let's build a `getBooks` API with AI. The template handles all the Stagehand setup for you.

You can develop with Stagehand in two ways:

* **Online IDE** — Zero setup. Write, test, and deploy directly from your browser.
* **CLI** — Use your favorite IDE with full version control.

<Tabs>
  <Tab title="Online IDE">
    <Steps>
      <Step title="Create a project">
        1. Go to [Intuned dashboard](https://app.intuned.io)
        2. Select **+ New Project** > **Templates** > **Stagehand**
        3. Select your language (Python or TypeScript)
        4. Name it and select **Create Project**

        <Frame>
          <img alt="Create Stagehand project from template" />
        </Frame>
      </Step>

      <Step title="Explore the project">
        The template includes a book scraper API that combines Playwright with AI agents.

        **Project structure:**

        <Tabs>
          <Tab title="TypeScript">
            ```
            my-stagehand-project/
            ├── api/
            │   └── get-books.ts             # Main automation API
            ├── hooks/
            │   └── setupContext.ts          # CDP URL setup
            ├── package.json                 # Dependencies
            └── intuned.json                 # Project configuration
            ```
          </Tab>

          <Tab title="Python">
            ```
            my-stagehand-project/
            ├── api/
            │   └── get-books.py             # Main automation API
            ├── hooks/
            │   └── setup_context.py         # CDP URL setup
            ├── pyproject.toml               # Dependencies
            └── intuned.json                 # Project configuration
            ```
          </Tab>
        </Tabs>

        **The automation code:**

        <CodeGroup>
          ```python Python theme={null}
          from typing import TypedDict
          from playwright.async_api import Page
          from pydantic import BaseModel
          from intuned_runtime import attempt_store, get_ai_gateway_config
          from stagehand import AsyncStagehand
          from stagehand.types.model_config_param import ModelConfigParam
          from stagehand.types.session_start_params import Browser, BrowserLaunchOptions


          class Params(TypedDict):
              category: str | None


          class BookDetails(BaseModel):
              title: str
              price: str
              rating: str | None = None
              availability: str | None = None


          class BooksResponse(BaseModel):
              books: list[BookDetails]


          MAX_PAGES = 10


          async def automation(page: Page, params: Params, **_kwargs):
              base_url, api_key = get_ai_gateway_config()
              cdp_url = attempt_store.get("cdp_url")

              model_name = "openai/gpt-5-mini"
              model_config: ModelConfigParam = {
                  "model_name": model_name,
                  "api_key": api_key,
                  "base_url": base_url,
                  "provider": "openai",
              }

              # Initialize Stagehand with act/extract/observe capabilities
              client = AsyncStagehand(
                  server="local",
                  model_api_key=api_key,
                  local_ready_timeout_s=30.0,
              )
              launch_options: BrowserLaunchOptions = {"headless": False}
              if cdp_url is not None:
                  launch_options["cdp_url"] = str(cdp_url)
              browser: Browser = {"type": "local", "launch_options": launch_options}
              session = await client.sessions.start(
                  model_name=model_name,
                  browser=browser,
              )
              session_id = session.data.session_id

              await page.set_viewport_size({"width": 1280, "height": 800})

              category = params.get("category")
              all_books: list[BookDetails] = []

              try:
                  await client.sessions.navigate(
                      id=session_id,
                      url="https://books.toscrape.com",
                  )

                  # Navigate to category if specified using observe and act
                  if category:
                      await client.sessions.observe(
                          id=session_id,
                          instruction=f'the "{category}" category link in the sidebar',
                          options={"model": model_config},
                      )
                      await client.sessions.act(
                          id=session_id,
                          input=f'Click on the "{category}" category link in the sidebar',
                          options={"model": model_config},
                      )

                  # Collect books from all pages
                  for page_num in range(1, MAX_PAGES + 1):
                      # Extract all book details from the current page
                      result = await client.sessions.extract(
                          id=session_id,
                          instruction="Extract all books on this page including title, price, rating and availability for each book",
                          options={"model": model_config},
                          schema={
                              "type": "object",
                              "properties": {
                                  "books": {
                                      "type": "array",
                                      "items": {
                                          "type": "object",
                                          "properties": {
                                              "title": {"type": "string"},
                                              "price": {"type": "string"},
                                              "rating": {"type": "string"},
                                              "availability": {"type": "string"},
                                          },
                                          "required": ["title", "price"],
                                      },
                                  }
                              },
                              "required": ["books"],
                          },
                      )

                      books_data = BooksResponse.model_validate(result.data.result)
                      all_books.extend(books_data.books)

                      # Check if there's a next page and navigate to it
                      if page_num < MAX_PAGES:
                          try:
                              next_button = await client.sessions.observe(
                                  id=session_id,
                                  instruction='the "next" button or link to go to the next page',
                                  options={"model": model_config},
                              )
                              if not next_button.data.result:
                                  break
                              await client.sessions.act(
                                  id=session_id,
                                  input='Click the "next" button to go to the next page',
                                  options={"model": model_config},
                              )
                          except Exception:
                              break

              finally:
                  await client.sessions.end(session_id)

              return BooksResponse(books=all_books)
          ```

          ```typescript TypeScript theme={null}
          import z from "zod";
          import { Stagehand } from "@browserbasehq/stagehand";
          import type { Page, BrowserContext } from "playwright";
          import { attemptStore, getAiGatewayConfig } from "@intuned/runtime";

          interface Params {
            category?: string;
          }

          const bookDetailsSchema = z.object({
            books: z.array(
              z.object({
                title: z.string(),
                price: z.string(),
                rating: z.string().optional(),
                availability: z.string().optional(),
              })
            ),
          });

          type BookDetails = z.infer<typeof bookDetailsSchema>["books"][number];
          type BooksResponse = z.infer<typeof bookDetailsSchema>;

          const MAX_PAGES = 10;

          async function getWebSocketUrl(cdpUrl: string): Promise<string> {
            if (cdpUrl.includes("ws://") || cdpUrl.includes("wss://")) {
              return cdpUrl;
            }
            const versionUrl = cdpUrl.endsWith("/")
              ? `${cdpUrl}json/version`
              : `${cdpUrl}/json/version`;
            const response = await fetch(versionUrl);
            const data = await response.json();
            return data.webSocketDebuggerUrl;
          }

          export default async function handler(
            { category }: Params,
            page: Page,
            _context: BrowserContext
          ) {
            const { baseUrl, apiKey } = await getAiGatewayConfig();
            const cdpUrl = attemptStore.get("cdpUrl") as string;
            const webSocketUrl = await getWebSocketUrl(cdpUrl);

            // Initialize Stagehand with observe/act/extract capabilities
            const stagehand = new Stagehand({
              env: "LOCAL",
              localBrowserLaunchOptions: {
                cdpUrl: webSocketUrl,
                viewport: { width: 1280, height: 800 },
              },
              model: {
                modelName: "openai/gpt-5-mini",
                apiKey,
                baseURL: baseUrl,
              },
            });
            await stagehand.init();

            await page.setViewportSize({ width: 1280, height: 800 });

            const allBooks: BookDetails[] = [];

            try {
              await page.goto("https://books.toscrape.com");

              // Navigate to category if specified using observe and act
              if (category) {
                await stagehand.observe(`the "${category}" category link in the sidebar`);
                await stagehand.act(`Click on the "${category}" category link in the sidebar`);
              }

              // Collect books from all pages
              for (let pageNum = 1; pageNum <= MAX_PAGES; pageNum++) {
                // Extract all book details from the current page
                const result = await stagehand.extract(
                  "Extract all books visible on the page with their complete details",
                  bookDetailsSchema
                );
                allBooks.push(...(result.books ?? []));

                // Check if there's a next page and navigate to it
                if (pageNum < MAX_PAGES) {
                  try {
                    const nextButton = await stagehand.observe(
                      'the "next" button or link to go to the next page'
                    );
                    if (!nextButton || nextButton.length === 0) {
                      break;
                    }
                    await stagehand.act('Click the "next" button to go to the next page');
                  } catch (e) {
                    break;
                  }
                }
              }
            } finally {
              await stagehand.close();
            }

            return { books: allBooks } satisfies BooksResponse;
          }
          ```
        </CodeGroup>

        <Info>
          **What this does:** Navigates to [https://books.toscrape.com](https://books.toscrape.com), uses Stagehand's `observe()` and `act()` methods to navigate to a specific category, then uses `extract()` to collect book details across multiple pages. TypeScript uses Zod schemas; Python uses a JSON schema dict validated into Pydantic models.
        </Info>
      </Step>

      <Step title="Run your automation">
        Run the book scraper to test your setup.

        1. In the Online IDE, select the API from the dropdown
        2. Select **Select Parameter** and enter `{"category": "Travel"}`
        3. Select **Start Run**

        <Frame>
          <img alt="Running Stagehand automation in IDE" />
        </Frame>
      </Step>

      <Step title="Deploy and test">
        Deploy your automation to Intuned's infrastructure.

        1. In the Online IDE, select **Deploy** in the top-right corner
        2. After deployment completes, go to the project's **Runs** page
        3. Select **Start Run**, then choose your API
        4. Enter parameters: `{"category": "Travel"}` and select **Start Run**

        <Frame>
          <img alt="Running Stagehand automation in Dashboard" />
        </Frame>

        5. After the run completes, view the extracted results

        <Frame>
          <img alt="View Stagehand run results" />
        </Frame>

        <Tip>
          Learn more about [Runs and how to trigger them programmatically](/docs/main/02-features/runs-single-executions).
        </Tip>
      </Step>
    </Steps>
  </Tab>

  <Tab title="CLI">
    <Steps>
      <Step title="Create a project">
        Run the following command and select your language (**Python** or **TypeScript**), then choose the **Stagehand** template:

        ```bash theme={null}
        npx create-intuned-project@latest
        ```

        Then navigate to your project:

        ```bash theme={null}
        cd my-stagehand-project
        ```
      </Step>

      <Step title="Explore the project">
        The template includes a book scraper API that combines Playwright with AI agents.

        **Project structure:**

        <Tabs>
          <Tab title="TypeScript">
            ```
            my-stagehand-project/
            ├── api/
            │   └── get-books.ts             # Main automation API
            ├── hooks/
            │   └── setupContext.ts          # CDP URL setup
            ├── package.json                 # Dependencies
            └── intuned.json                 # Project configuration
            ```
          </Tab>

          <Tab title="Python">
            ```
            my-stagehand-project/
            ├── api/
            │   └── get-books.py             # Main automation API
            ├── hooks/
            │   └── setup_context.py         # CDP URL setup
            ├── pyproject.toml               # Dependencies
            └── intuned.json                 # Project configuration
            ```
          </Tab>
        </Tabs>

        **The automation code:**

        <CodeGroup>
          ```python Python theme={null}
          from typing import TypedDict
          from playwright.async_api import Page
          from pydantic import BaseModel
          from intuned_runtime import attempt_store, get_ai_gateway_config
          from stagehand import AsyncStagehand
          from stagehand.types.model_config_param import ModelConfigParam
          from stagehand.types.session_start_params import Browser, BrowserLaunchOptions


          class Params(TypedDict):
              category: str | None


          class BookDetails(BaseModel):
              title: str
              price: str
              rating: str | None = None
              availability: str | None = None


          class BooksResponse(BaseModel):
              books: list[BookDetails]


          MAX_PAGES = 10


          async def automation(page: Page, params: Params, **_kwargs):
              base_url, api_key = get_ai_gateway_config()
              cdp_url = attempt_store.get("cdp_url")

              model_name = "openai/gpt-5-mini"
              model_config: ModelConfigParam = {
                  "model_name": model_name,
                  "api_key": api_key,
                  "base_url": base_url,
                  "provider": "openai",
              }

              # Initialize Stagehand with act/extract/observe capabilities
              client = AsyncStagehand(
                  server="local",
                  model_api_key=api_key,
                  local_ready_timeout_s=30.0,
              )
              launch_options: BrowserLaunchOptions = {"headless": False}
              if cdp_url is not None:
                  launch_options["cdp_url"] = str(cdp_url)
              browser: Browser = {"type": "local", "launch_options": launch_options}
              session = await client.sessions.start(
                  model_name=model_name,
                  browser=browser,
              )
              session_id = session.data.session_id

              await page.set_viewport_size({"width": 1280, "height": 800})

              category = params.get("category")
              all_books: list[BookDetails] = []

              try:
                  await client.sessions.navigate(
                      id=session_id,
                      url="https://books.toscrape.com",
                  )

                  # Navigate to category if specified using observe and act
                  if category:
                      await client.sessions.observe(
                          id=session_id,
                          instruction=f'the "{category}" category link in the sidebar',
                          options={"model": model_config},
                      )
                      await client.sessions.act(
                          id=session_id,
                          input=f'Click on the "{category}" category link in the sidebar',
                          options={"model": model_config},
                      )

                  # Collect books from all pages
                  for page_num in range(1, MAX_PAGES + 1):
                      # Extract all book details from the current page
                      result = await client.sessions.extract(
                          id=session_id,
                          instruction="Extract all books on this page including title, price, rating and availability for each book",
                          options={"model": model_config},
                          schema={
                              "type": "object",
                              "properties": {
                                  "books": {
                                      "type": "array",
                                      "items": {
                                          "type": "object",
                                          "properties": {
                                              "title": {"type": "string"},
                                              "price": {"type": "string"},
                                              "rating": {"type": "string"},
                                              "availability": {"type": "string"},
                                          },
                                          "required": ["title", "price"],
                                      },
                                  }
                              },
                              "required": ["books"],
                          },
                      )

                      books_data = BooksResponse.model_validate(result.data.result)
                      all_books.extend(books_data.books)

                      # Check if there's a next page and navigate to it
                      if page_num < MAX_PAGES:
                          try:
                              next_button = await client.sessions.observe(
                                  id=session_id,
                                  instruction='the "next" button or link to go to the next page',
                                  options={"model": model_config},
                              )
                              if not next_button.data.result:
                                  break
                              await client.sessions.act(
                                  id=session_id,
                                  input='Click the "next" button to go to the next page',
                                  options={"model": model_config},
                              )
                          except Exception:
                              break

              finally:
                  await client.sessions.end(session_id)

              return BooksResponse(books=all_books)
          ```

          ```typescript TypeScript theme={null}
          import z from "zod";
          import { Stagehand } from "@browserbasehq/stagehand";
          import type { Page, BrowserContext } from "playwright";
          import { attemptStore, getAiGatewayConfig } from "@intuned/runtime";

          interface Params {
            category?: string;
          }

          const bookDetailsSchema = z.object({
            books: z.array(
              z.object({
                title: z.string(),
                price: z.string(),
                rating: z.string().optional(),
                availability: z.string().optional(),
              })
            ),
          });

          type BookDetails = z.infer<typeof bookDetailsSchema>["books"][number];
          type BooksResponse = z.infer<typeof bookDetailsSchema>;

          const MAX_PAGES = 10;

          async function getWebSocketUrl(cdpUrl: string): Promise<string> {
            if (cdpUrl.includes("ws://") || cdpUrl.includes("wss://")) {
              return cdpUrl;
            }
            const versionUrl = cdpUrl.endsWith("/")
              ? `${cdpUrl}json/version`
              : `${cdpUrl}/json/version`;
            const response = await fetch(versionUrl);
            const data = await response.json();
            return data.webSocketDebuggerUrl;
          }

          export default async function handler(
            { category }: Params,
            page: Page,
            _context: BrowserContext
          ) {
            const { baseUrl, apiKey } = await getAiGatewayConfig();
            const cdpUrl = attemptStore.get("cdpUrl") as string;
            const webSocketUrl = await getWebSocketUrl(cdpUrl);

            // Initialize Stagehand with observe/act/extract capabilities
            const stagehand = new Stagehand({
              env: "LOCAL",
              localBrowserLaunchOptions: {
                cdpUrl: webSocketUrl,
                viewport: { width: 1280, height: 800 },
              },
              model: {
                modelName: "openai/gpt-4o",
                apiKey,
                baseURL: baseUrl,
              },
            });
            await stagehand.init();

            await page.setViewportSize({ width: 1280, height: 800 });

            const allBooks: BookDetails[] = [];

            try {
              await page.goto("https://books.toscrape.com");

              // Navigate to category if specified using observe and act
              if (category) {
                await stagehand.observe(`the "${category}" category link in the sidebar`);
                await stagehand.act(`Click on the "${category}" category link in the sidebar`);
              }

              // Collect books from all pages
              for (let pageNum = 1; pageNum <= MAX_PAGES; pageNum++) {
                // Extract all book details from the current page
                const result = await stagehand.extract(
                  "Extract all books visible on the page with their complete details",
                  bookDetailsSchema
                );
                allBooks.push(...(result.books ?? []));

                // Check if there's a next page and navigate to it
                if (pageNum < MAX_PAGES) {
                  try {
                    const nextButton = await stagehand.observe(
                      'the "next" button or link to go to the next page'
                    );
                    if (!nextButton || nextButton.length === 0) {
                      break;
                    }
                    await stagehand.act('Click the "next" button to go to the next page');
                  } catch (e) {
                    break;
                  }
                }
              }
            } finally {
              await stagehand.close();
            }

            return { books: allBooks } satisfies BooksResponse;
          }
          ```
        </CodeGroup>

        <Info>
          **What this does:** Navigates to [https://books.toscrape.com](https://books.toscrape.com), uses Stagehand's `observe()` and `act()` methods to navigate to a specific category, then uses `extract()` to collect book details across multiple pages. TypeScript uses Zod schemas; Python uses a JSON schema dict validated into Pydantic models.
        </Info>
      </Step>

      <Step title="Run locally">
        Test your automation locally:

        <CLICommandTabs />

        The automation navigates to the category and extracts book details.
      </Step>

      <Step title="Deploy and test">
        Deploy your automation using the CLI:

        <CLICommandTabs />

        After deployment, test in the Dashboard:

        1. Go to the project's **Runs** page
        2. Select **Start Run** and select your API
        3. Enter parameters: `{"category": "Travel"}` and select **Start Run**

        <Frame>
          <img alt="Running Stagehand automation in Dashboard" />
        </Frame>

        4. After the run completes, view the extracted results

        <Frame>
          <img alt="View Stagehand run results" />
        </Frame>

        <Tip>
          Learn more about [Runs and how to trigger them programmatically](/docs/main/02-features/runs-single-executions).
        </Tip>
      </Step>
    </Steps>
  </Tab>
</Tabs>

## How it works

* The `setupContext` hook stores the CDP URL in [`attemptStore`](/docs/main/05-references/runtime-sdk-python/attempt-store) for your API to access

<Tip>
  Check out our [setup hook recipe](/docs/main/01-learn/recipes/setup-hooks).
</Tip>

* Your API initializes Stagehand with the CDP URL and AI gateway credentials from `getAiGatewayConfig()`
* Stagehand provides three core AI-powered methods:
  * `observe()` - Find elements on the page using natural language
  * `act()` - Perform actions like clicking using natural language
  * `extract()` - Extract structured data from the page with type-safe schemas

<Tabs>
  <Tab title="TypeScript">
    - Passes AI credentials via the `model` config (`modelName`, `apiKey`, `baseURL`)
    - Call methods directly on `stagehand` (e.g., `stagehand.act()`, `stagehand.extract()`)
    - Use the Playwright `page` for navigation (e.g., `page.goto()`)
  </Tab>

  <Tab title="Python">
    * Uses `AsyncStagehand` with a sessions-based API — start a session, then call methods via `client.sessions`
    * Navigate with `client.sessions.navigate()`, act/observe/extract with `client.sessions.act/observe/extract()`
    * Pass a JSON schema dict to `extract()` and validate the result with `BooksResponse.model_validate()`
    * End the session with `client.sessions.end(session_id)` in a `finally` block
    * Uses `get_ai_gateway_config()` to get AI credentials and passes them via `options={"model": model_config}` on each call
  </Tab>
</Tabs>

* Your API accepts parameters (like `category`) that can vary for each run
* The automation handles pagination automatically and cleans up Stagehand when done

## Related resources

<CardGroup>
  <Card title="Stagehand Documentation" icon="book" href="https://docs.stagehand.dev">
    Official Stagehand documentation and API reference
  </Card>

  <Card title="Stagehand GitHub" icon="github" href="https://github.com/browserbase/stagehand">
    View the source code and contribute to Stagehand
  </Card>

  <Card title="Intuned Cookbook" icon="code" href="https://github.com/Intuned/cookbook">
    Browse examples and recipes for Intuned projects
  </Card>

  <Card title="attemptStore Reference" icon="database" href="/main/05-references/runtime-sdk-python/attempt-store">
    Learn more about sharing data between hooks and APIs
  </Card>
</CardGroup>


# Supabase
Source: https://intunedhq.com/docs/main/04-integrations/supabase



## Overview

By the end of this guide, you'll have an Intuned project with a scraping Job that sends data to Supabase for processing and storage. You'll:

1. Set up Supabase Storage or an Edge Function endpoint for Intuned.
2. Configure a Job with a Supabase-compatible sink.
3. Trigger a Job and verify data lands in your database.

## Prerequisites

Before you begin, ensure you have the following:

* A [Supabase project](https://supabase.com/dashboard)
* An [Intuned account](https://app.intuned.io)
* The e-commerce scraper project from the [quick start](/docs/main/00-getting-started/quickstarts/scraper) deployed to your workspace.

<Note>
  This guide assumes you have a basic understanding of Intuned Projects and Jobs. If you're new to Intuned, start with the [getting started guide](/docs/main/00-getting-started/introduction).
</Note>

## When to use Supabase integration

Scrapers built on Intuned typically run via Jobs on a schedule. When a JobRun completes, you want that data sent somewhere for processing or persistence.The Supabase integration delivers scraped data to your Supabase database.

<Note>
  While this guide focuses on scraping, Supabase integration works for any Intuned Job—delivering Run results from any automation.
</Note>

## Choose your approach

This guide covers two approaches for connecting Intuned with Supabase:

| Consideration      | Supabase Storage                     | Webhook                              |
| ------------------ | ------------------------------------ | ------------------------------------ |
| Delivery guarantee | Intuned guarantees file delivery     | Your endpoint must handle failures   |
| Reprocessing       | Re-trigger database webhooks anytime | Not possible without re-running Jobs |
| Complexity         | Requires storage + webhook setup     | Simpler setup                        |
| Debugging          | Inspect files in storage + logs      | Check Edge Function logs             |

**Recommendation:** Use Supabase Storage for production workloads. Intuned guarantees results are written to your bucket, and you can always reprocess by re-triggering the database webhook.

## Guide

### Create a products table

Both approaches require a table to store the scraped data. In your Supabase [SQL editor](https://supabase.com/main/guides/database/overview#the-sql-editor), run:

```sql theme={null}
create table products (
  id serial primary key,
  name text unique not null,
  price numeric not null,
  url text,
  sku text,
  intuned_run_id text not null,
  created_at timestamptz default now()
);
```

### Set up the integration

<Tabs>
  <Tab title="Supabase Storage">
    **Best for:** Reliability and reprocessing capability

    Intuned writes results to Supabase Storage, then a database trigger invokes an Edge Function to process each file. This approach guarantees data delivery and lets you reprocess files if needed.

    <Steps>
      <Step title="Create a storage bucket">
        1. Go to your [Supabase dashboard](https://supabase.com/dashboard)
        2. Select **Storage** from the sidebar
        3. Select **New bucket**
        4. Name it `ecommerce-ingest`
        5. Keep it as a private bucket

        See [Supabase Storage quickstart](https://supabase.com/main/guides/storage/quickstart#create-a-bucket) for details.
      </Step>

      <Step title="Enable S3 compatibility">
        1. In Storage, select **S3** under Configuration
        2. Turn on **S3 protocol connection**

        <Frame>
          <img alt="Supabase S3 Configuration showing endpoint and region" />
        </Frame>

        Note your **Endpoint** and **Region** values.
      </Step>

      <Step title="Create access keys">
        1. In the S3 Configuration page, select **New access key**
        2. Copy both the **Access key ID** and **Secret access key**

        <Frame>
          <img alt="Supabase S3 access keys modal" />
        </Frame>

        <Warning>
          Save these credentials securely. The secret key won't be shown again.
        </Warning>

        See [Supabase S3 authentication](https://supabase.com/main/guides/storage/s3/authentication) for more details.
      </Step>

      <Step title="Create a Job with Supabase Storage sink">
        <Tabs>
          <Tab title="Dashboard">
            1. Go to [app.intuned.io](https://app.intuned.io)
            2. Open your `ecommerce-scraper-quickstart` project
            3. Select the **Jobs** tab
            4. Select **Create Job**
            5. Fill in the Job ID and payloads
            6. Enable **Sink**
            7. Select **S3 Compatible**
            8. Enter your Supabase Storage credentials:
               * **Bucket**: `ecommerce-ingest`
               * **Region**: Your region (e.g., `us-east-2`)
               * **Access Key ID**: Your S3 access key
               * **Secret Access Key**: Your S3 secret key
               * **Prefix**: `ecommerce-quickstart/supabase-bucket/` (optional)
               * **Custom endpoint**: Your Supabase Storage endpoint
            9. Select **Force path-style URLs**
            10. Select **Create Job**

            <Frame>
              <img alt="Intuned Job configuration with S3 compatible sink for Supabase Storage" />
            </Frame>
          </Tab>

          <Tab title="TypeScript SDK">
            ```typescript theme={null}
            import { IntunedClient } from "@intuned/client";

            const intunedClient = new IntunedClient({
              workspaceId: "your-workspace-id",
              apiKey: process.env["INTUNED_API_KEY"] ?? "",
            });

            async function createJobWithSupabaseStorage() {
              const result = await intunedClient.projects.jobs.create(
                "ecommerce-scraper-quickstart",
                {
                  id: "supabase-storage",
                  payload: [
                    {
                      apiName: "list",
                      parameters: {},
                    },
                  ],
                  configuration: {
                    retry: {
                      maximumAttempts: 3,
                    },
                  },
                  sink: {
                    type: "s3",
                    bucket: "ecommerce-ingest",
                    region: "us-east-2",
                    accessKeyId: process.env["SUPABASE_S3_ACCESS_KEY_ID"]!,
                    secretAccessKey: process.env["SUPABASE_S3_SECRET_ACCESS_KEY"]!,
                    prefix: "ecommerce-quickstart/supabase-bucket/",
                    endpoint: "https://your-project.supabase.co/storage/v1/s3",
                    forcePathStyle: true,
                  },
                }
              );

              console.log(result);
            }

            createJobWithSupabaseStorage();
            ```

            <Tip>
              Store your Supabase S3 credentials in environment variables rather than hardcoding them.
            </Tip>
          </Tab>

          <Tab title="Python SDK">
            ```python theme={null}
            from intuned_client import IntunedClient
            from intuned_client import models
            import os

            with IntunedClient(
                workspace_id="your-workspace-id",
                api_key=os.getenv("INTUNED_API_KEY", ""),
            ) as ic_client:
                result = ic_client.projects.jobs.create(
                    project_name="ecommerce-scraper-quickstart",
                    body=models.JobsCreateRequestBody(
                        id="supabase-storage",
                        payload=[
                            {
                                "apiName": "list",
                                "parameters": {},
                            },
                        ],
                        configuration={
                            "retry": {
                                "maximumAttempts": 3,
                            },
                        },
                        sink={
                            "type": "s3",
                            "bucket": "ecommerce-ingest",
                            "region": "us-east-2",
                            "accessKeyId": os.getenv("SUPABASE_S3_ACCESS_KEY_ID"),
                            "secretAccessKey": os.getenv("SUPABASE_S3_SECRET_ACCESS_KEY"),
                            "prefix": "ecommerce-quickstart/supabase-bucket/",
                            "endpoint": "https://your-project.supabase.co/storage/v1/s3",
                            "forcePathStyle": True,
                        },
                    ),
                )

                print(result)
            ```

            <Tip>
              Store your Supabase S3 credentials in environment variables rather than hardcoding them.
            </Tip>
          </Tab>
        </Tabs>

        For advanced S3 sink options, see the [AWS S3 integration](/docs/main/04-integrations/s3/s3-job-sink#configuration-options).
      </Step>

      <Step title="Trigger the Job">
        <Tabs>
          <Tab title="Dashboard">
            1. In the Jobs tab, find your Job
            2. Select **Actions** > **Trigger**

            <Frame>
              <img alt="Triggering the Intuned Job" />
            </Frame>

            Results appear in your Supabase Storage bucket as JSON files.

            <Frame>
              <img alt="Supabase Storage bucket with Intuned result files" />
            </Frame>
          </Tab>

          <Tab title="TypeScript SDK">
            ```typescript theme={null}
            import { IntunedClient } from "@intuned/client";

            const intunedClient = new IntunedClient({
              workspaceId: "your-workspace-id",
              apiKey: process.env["INTUNED_API_KEY"] ?? "",
            });

            async function run() {
              const result = await intunedClient.projects.jobs.trigger(
                "ecommerce-scraper-quickstart",
                "supabase-storage"
              );

              console.log(result);
            }

            run();
            ```
          </Tab>

          <Tab title="Python SDK">
            ```python theme={null}
            from intuned_client import IntunedClient
            import os

            with IntunedClient(
                workspace_id="your-workspace-id",
                api_key=os.getenv("INTUNED_API_KEY", ""),
            ) as ic_client:
                result = ic_client.projects.jobs.trigger(
                    project_name="ecommerce-scraper-quickstart",
                    job_id="supabase-storage"
                )

                print(result)
            ```
          </Tab>
        </Tabs>
      </Step>

      <Step title="Create the processing Edge Function">
        Create an Edge Function that processes files when they're added to storage.

        1. Go to **Edge Functions** in your [Supabase dashboard](https://supabase.com/dashboard)
        2. Select **Create a new function**
        3. Name it `intuned-ingest-bucket`

        <Frame>
          <img alt="Creating the bucket processing Edge Function" />
        </Frame>

        Replace the default code with:

        ```typescript theme={null}
        import "jsr:@supabase/functions-js/edge-runtime.d.ts";
        import { createClient } from "jsr:@supabase/supabase-js@2";

        const BUCKET_NAME = "ecommerce-ingest";
        const PATH_PREFIX = "ecommerce-quickstart/supabase-bucket/";

        interface WebhookPayload {
          type: "INSERT" | "UPDATE" | "DELETE";
          table: string;
          schema: string;
          record: {
            id: string;
            bucket_id: string;
            name: string;
            metadata: { size: number; mimetype: string } | null;
          };
          old_record: null;
        }

        Deno.serve(async (req: Request) => {
          const payload: WebhookPayload = await req.json();
          const { record } = payload;

          // Skip RLS test inserts (no metadata)
          if (!record.metadata) {
            return new Response(
              JSON.stringify({ processed: false, reason: "no metadata" }),
              { status: 200 }
            );
          }

          // Only process JSON files in the correct bucket and path
          if (
            record.bucket_id !== BUCKET_NAME ||
            !record.name.startsWith(PATH_PREFIX) ||
            !record.name.endsWith(".json")
          ) {
            return new Response(
              JSON.stringify({ processed: false, reason: "path mismatch" }),
              { status: 200 }
            );
          }

          const supabase = createClient(
            Deno.env.get("SUPABASE_URL")!,
            Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
          );

          // Download the JSON file
          const { data: fileData, error: downloadError } = await supabase.storage
            .from(record.bucket_id)
            .download(record.name);

          if (downloadError) {
            console.error("Download error:", downloadError);
            return new Response(
              JSON.stringify({ error: downloadError.message }),
              { status: 500 }
            );
          }

          const jsonContent = JSON.parse(await fileData.text());
          const result = jsonContent.apiInfo.result.result;

          // Transform the product data
          const product = {
            name: result.product.name,
            price: parseFloat(result.product.price.replace(/[^0-9.]/g, "")) || 0,
            url: result.product.detailsUrl,
            sku: result.product.sku,
            intuned_run_id: jsonContent.apiInfo.runId,
          };

          const { error } = await supabase
            .from("products")
            .upsert([product], { onConflict: "name" });

          if (error) {
            console.error("Insert error:", error);
            return new Response(
              JSON.stringify({ error: error.message }),
              { status: 500 }
            );
          }

          return new Response(
            JSON.stringify({ processed: true, inserted: 1 }),
            { status: 200 }
          );
        });
        ```

        Select **Deploy function** to save.

        See [Supabase Edge Functions](https://supabase.com/main/guides/functions) for more details.
      </Step>

      <Step title="Create the database webhook">
        Set up a database webhook to trigger your Edge Function when files are added to storage.

        1. Go to **Database** > **Webhooks** in your [Supabase dashboard](https://supabase.com/dashboard)
        2. If prompted, select **Enable Webhooks**
        3. Select **Create a new hook**
        4. Configure the webhook:
           * **Name**: `intuned-bucket-trigger`
           * **Table**: Select `storage.objects` from the dropdown
           * **Events**: Select **Insert**
           * **Type**: Select **Supabase Edge Functions**
           * **Edge Function**: Select `intuned-ingest-bucket`
        5. Select **Create webhook**

        See [Supabase database webhooks](https://supabase.com/main/guides/database/webhooks) for more details.
      </Step>

      <Step title="Verify the results">
        After triggering the Job, check your Supabase products table. As files are written to storage, the database webhook fires and your Edge Function processes each one.

        <Frame>
          <img alt="Supabase products table populated with scraped data" />
        </Frame>
      </Step>
    </Steps>
  </Tab>

  <Tab title="Webhook to Edge Function">
    **Best for:** Real-time processing with simple setup

    Data flows directly from Intuned to your Edge Function, which inserts it into your database. Simpler to set up, but you'll need to handle failures in your Edge Function.

    For more webhook options, see the [Webhook integration](/docs/main/04-integrations/webhook).

    <Steps>
      <Step title="Create a new Edge Function">
        1. Go to your [Supabase dashboard](https://supabase.com/dashboard)
        2. Select **Edge Functions** from the sidebar
        3. Select **Create a new function**
        4. Name it `intuned-ingest`

        Replace the default code with:

        ```typescript theme={null}
        import "jsr:@supabase/functions-js/edge-runtime.d.ts";
        import { createClient } from "jsr:@supabase/supabase-js@2";

        Deno.serve(async (req: Request) => {
          const payload = await req.json();
          const result = payload.apiInfo.result.result;

          // Transform the product data
          const product = {
            name: result.product.name,
            price: parseFloat(result.product.price.replace(/[^0-9.]/g, "")) || 0,
            url: result.product.detailsUrl,
            sku: result.product.sku,
            intuned_run_id: payload.apiInfo.runId,
          };

          // Insert into database
          const supabase = createClient(
            Deno.env.get("SUPABASE_URL")!,
            Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
          );

          const { error } = await supabase
            .from("products")
            .upsert([product], { onConflict: "name" });

          if (error) {
            console.error("Insert error:", error);
            return new Response(
              JSON.stringify({ error: error.message }),
              { status: 500 }
            );
          }

          return new Response(
            JSON.stringify({ received: true, inserted: 1 }),
            { status: 200 }
          );
        });
        ```

        Select **Deploy function** to save.

        See [Supabase Edge Functions](https://supabase.com/main/guides/functions) for more details.
      </Step>

      <Step title="Get function credentials">
        After deployment, go to the **Details** tab of your Edge Function. You'll need the **Endpoint URL** and **anon key** for the Intuned configuration.

        <Frame>
          <img alt="Supabase Edge Function details showing endpoint URL and anon key" />
        </Frame>

        Select **Show anon key** to reveal the key value.

        <Warning>
          Make sure **Verify JWT with legacy secret** is turned off in the Function Configuration section. This setting should be disabled for the webhook to work correctly.
        </Warning>
      </Step>

      <Step title="Create a Job with webhook sink">
        <Tabs>
          <Tab title="Dashboard">
            1. Go to [app.intuned.io](https://app.intuned.io)
            2. Open your `ecommerce-scraper-quickstart` project
            3. Select the **Jobs** tab
            4. Select **Create Job**
            5. Fill in the Job ID and payloads
            6. Enable **Sink**
            7. Select **Webhook**
            8. Enter your Edge Function URL
            9. Add the required headers:
               * `apikey`: Your Supabase anon key
               * `Authorization`: `Bearer <your-anon-key>`
            10. Select **Create Job**

            <Frame>
              <img alt="Intuned Job configuration with webhook sink for Supabase" />
            </Frame>
          </Tab>

          <Tab title="TypeScript SDK">
            ```typescript theme={null}
            import { IntunedClient } from "@intuned/client";

            const intunedClient = new IntunedClient({
              workspaceId: "your-workspace-id",
              apiKey: process.env["INTUNED_API_KEY"] ?? "",
            });

            async function createJobWithWebhook() {
              const result = await intunedClient.projects.jobs.create(
                "ecommerce-scraper-quickstart",
                {
                  id: "supabase-webhook",
                  payload: [
                    {
                      apiName: "list",
                      parameters: {},
                    },
                  ],
                  configuration: {
                    retry: {
                      maximumAttempts: 3,
                    },
                  },
                  sink: {
                    type: "webhook",
                    url: "https://your-project.supabase.co/functions/v1/intuned-ingest",
                    headers: {
                      apikey: process.env["SUPABASE_ANON_KEY"]!,
                      Authorization: `Bearer ${process.env["SUPABASE_ANON_KEY"]}`,
                    },
                  },
                }
              );

              console.log(result);
            }

            createJobWithWebhook();
            ```

            <Tip>
              Store your Supabase anon key in environment variables rather than hardcoding it.
            </Tip>
          </Tab>

          <Tab title="Python SDK">
            ```python theme={null}
            from intuned_client import IntunedClient
            from intuned_client import models
            import os

            with IntunedClient(
                workspace_id="your-workspace-id",
                api_key=os.getenv("INTUNED_API_KEY", ""),
            ) as ic_client:
                supabase_anon_key = os.getenv("SUPABASE_ANON_KEY")

                result = ic_client.projects.jobs.create(
                    project_name="ecommerce-scraper-quickstart",
                    body=models.JobsCreateRequestBody(
                        id="supabase-webhook",
                        payload=[
                            {
                                "apiName": "list",
                                "parameters": {},
                            },
                        ],
                        configuration={
                            "retry": {
                                "maximumAttempts": 3,
                            },
                        },
                        sink={
                            "type": "webhook",
                            "url": "https://your-project.supabase.co/functions/v1/intuned-ingest",
                            "headers": {
                                "apikey": supabase_anon_key,
                                "Authorization": f"Bearer {supabase_anon_key}",
                            },
                        },
                    ),
                )

                print(result)
            ```

            <Tip>
              Store your Supabase anon key in environment variables rather than hardcoding it.
            </Tip>
          </Tab>
        </Tabs>

        For advanced webhook options like retry handling and authentication, see the [Webhook integration](/docs/main/04-integrations/webhook).
      </Step>

      <Step title="Trigger the Job">
        <Tabs>
          <Tab title="Dashboard">
            1. In the Jobs tab, find your Job
            2. Select **Actions** > **Trigger**

            <Frame>
              <img alt="Triggering the Intuned Job" />
            </Frame>
          </Tab>

          <Tab title="TypeScript SDK">
            ```typescript theme={null}
            import { IntunedClient } from "@intuned/client";

            const intunedClient = new IntunedClient({
              workspaceId: "your-workspace-id",
              apiKey: process.env["INTUNED_API_KEY"] ?? "",
            });

            async function run() {
              const result = await intunedClient.projects.jobs.trigger(
                "ecommerce-scraper-quickstart",
                "supabase-webhook"
              );

              console.log(result);
            }

            run();
            ```
          </Tab>

          <Tab title="Python SDK">
            ```python theme={null}
            from intuned_client import IntunedClient
            import os

            with IntunedClient(
                workspace_id="your-workspace-id",
                api_key=os.getenv("INTUNED_API_KEY", ""),
            ) as ic_client:
                result = ic_client.projects.jobs.trigger(
                    project_name="ecommerce-scraper-quickstart",
                    job_id="supabase-webhook"
                )

                print(result)
            ```
          </Tab>
        </Tabs>
      </Step>

      <Step title="Verify the results">
        After triggering the Job, check your Supabase products table. As each Run completes, Intuned sends the results to your Edge Function, which inserts them into your database.

        <Frame>
          <img alt="Supabase products table populated with scraped data" />
        </Frame>
      </Step>
    </Steps>
  </Tab>
</Tabs>

## Data format

Intuned sends Run results as JSON. The payload structure includes metadata about the Run and the extracted data:

```json theme={null}
{
  "apiInfo": {
    "runId": "run_abc123",
    "apiName": "list",
    "result": {
      "status": "success",
      "result": {
        "product": {
          "name": "Example Product",
          "price": "$29.99",
          "detailsUrl": "https://example.com/product",
          "sku": "SKU-12345"
        }
      }
    }
  }
}
```

Your Edge Function code extracts the nested `result` object and transforms it for your database schema.

## Troubleshooting

### Edge Function not receiving data

**Cause:** The Edge Function isn't receiving webhook requests from Intuned. Common reasons include incorrect endpoint URL, missing authentication headers, or the function not being deployed.

**Solution:** Verify the endpoint URL matches your Edge Function URL exactly. Check that `apikey` and `Authorization` headers are set correctly with your Supabase anon key. Confirm the function is deployed in your Supabase dashboard. Ensure **Verify JWT with legacy secret** is turned off.

### Files not appearing in storage

**Cause:** Intuned can't write files to your Supabase Storage bucket. This typically happens with incorrect S3 credentials, missing path-style URL configuration, or wrong bucket name.

**Solution:** Regenerate S3 access keys in Supabase and update your Job configuration. Enable **Force path-style URLs** in the Job sink settings. Verify the bucket name and endpoint match your Supabase project exactly.

### Database webhook not triggering

**Cause:** The database webhook isn't firing when files are added to storage. The webhook may be configured for the wrong table or event type.

**Solution:** Verify the webhook targets the `storage.objects` table. Ensure the **Insert** event is selected. Check Edge Function logs for errors. Make sure the Edge Function is selected as the webhook target.

### Data not inserting into products table

**Cause:** The Edge Function is receiving data but can't insert it into your database. Common issues include table schema mismatch, unique constraint violations, or missing database permissions.

**Solution:** Verify your products table matches the schema from the "Create a products table" step. Check if products with the same name already exist (the `upsert` should handle this, but verify the `onConflict` clause). Ensure the Edge Function uses the service role key for database access.

### Job paused with sink error

**Cause:** For S3-compatible sinks, Intuned automatically pauses the Job when it fails to write data to storage.

**Solution:** Check the Job status in the Intuned dashboard (shows as "Paused"). Fix the underlying credential or configuration issue. Update the Job configuration if needed, then select **Resume** from the dashboard. The Job continues from where it paused.

## Related resources

<CardGroup>
  <Card title="AWS S3 integration" icon="cloud" href="/main/04-integrations/s3/s3-job-sink">
    Advanced S3 sink configuration and best practices
  </Card>

  <Card title="Webhook integration" icon="webhook" href="/main/04-integrations/webhook">
    Advanced webhook configuration and data processing patterns
  </Card>

  <Card title="Jobs" icon="clock" href="/main/02-features/jobs-batched-executions">
    Set up scheduled and batched Job executions
  </Card>

  <Card title="Supabase Edge Functions" icon="bolt" href="https://supabase.com/main/guides/functions">
    Official Supabase Edge Functions documentation
  </Card>
</CardGroup>


# Webhook
Source: https://intunedhq.com/docs/main/04-integrations/webhook



## Overview

By the end of this guide, you'll have an Intuned project (+ scraping Job with webhook sink) that sends scraped data directly to your endpoint. You'll:

1. Set up a webhook endpoint to receive data from Intuned.
2. Configure a Job with a webhook sink.
3. Trigger a Job and verify data arrives at your endpoint.

## Prerequisites

Before you begin, ensure you have the following:

* An Intuned account.
* A backend service capable of accepting HTTP POST requests.
* Your webhook endpoint exposed to the internet (publicly accessible URL).

<Note>This guide assumes you have a basic understanding of Intuned Projects and Jobs. If you're new to Intuned, start with the [getting started guide](/docs/main/00-getting-started/introduction).</Note>

## When to use webhook integration

Scrapers built on Intuned typically run via Jobs on a schedule. When a JobRun completes, you want that data sent somewhere for processing.

Webhook integration delivers scraped data to your endpoint in real-time as each API Run completes. This enables instant processing without polling—your backend receives results the moment they're ready.

<Note>While this guide focuses on scraping, webhook integration works for any Intuned Job—webhooks deliver Run results from any automation.</Note>

## Guide

### 1. Set up a webhook endpoint

Add an endpoint to your backend that accepts POST requests from Intuned. The endpoint should parse JSON payloads and return a 200 status quickly to avoid timeouts.

#### Endpoint code

Add this route to your existing backend:

<Tabs>
  <Tab title="Express (Node.js)">
    ```typescript theme={null}
    // Types for the webhook payload
    interface WebhookPayload {
      workspaceId: string;
      project: { id: string; name: string };
      projectJob: { id: string };
      projectJobRun: { id: string };
      apiInfo: {
        name: string;
        parameters: any;
        runId: string;
        result: {
          status: 'completed' | 'failed';
          result?: any;
          error?: string;
          message?: string;
        };
      };
    }

    // Add this endpoint to your Express app
    app.post('/webhooks/intuned', async (req, res) => {
      const payload = req.body as WebhookPayload;

      console.log('Received webhook from Intuned');
      console.log(`Project: ${payload.project.name}`);
      console.log(`JobRun: ${payload.projectJobRun.id}`);
      console.log(`Status: ${payload.apiInfo.result.status}`);

      // Acknowledge receipt immediately
      res.status(200).json({ received: true });

      // Process the data asynchronously
      processWebhookData(payload).catch(err => {
        console.error('Error processing webhook:', err);
      });
    });

    async function processWebhookData(payload: WebhookPayload) {
      if (payload.apiInfo.result.status === 'completed') {
        console.log('Data:', payload.apiInfo.result.result);
        // Add your processing logic here
      } else {
        console.error('Failed:', payload.apiInfo.result.message);
      }
    }
    ```
  </Tab>

  <Tab title="Flask (Python)">
    ```python theme={null}
    from flask import request, jsonify
    from typing import Dict, Any

    # Add this route to your Flask app
    @app.route('/webhooks/intuned', methods=['POST'])
    def intuned_webhook():
        payload: Dict[str, Any] = request.get_json()

        app.logger.info('Received webhook from Intuned')
        app.logger.info(f"Project: {payload['project']['name']}")
        app.logger.info(f"JobRun: {payload['projectJobRun']['id']}")
        app.logger.info(f"Status: {payload['apiInfo']['result']['status']}")

        # Acknowledge receipt immediately
        response = jsonify({'received': True})

        # Process the data asynchronously
        try:
            process_webhook_data(payload)
        except Exception as e:
            app.logger.error(f'Error processing webhook: {e}')

        return response, 200

    def process_webhook_data(payload: Dict[str, Any]):
        result = payload['apiInfo']['result']

        if result['status'] == 'completed':
            app.logger.info(f"Data: {result.get('result')}")
            # Add your processing logic here
        else:
            app.logger.error(f"Failed: {result.get('message')}")
    ```
  </Tab>
</Tabs>

<Accordion title="Need a test server? Create one from scratch">
  If you don't have an existing backend and want to test webhooks, create a minimal server:

  <Tabs>
    <Tab title="Express (Node.js)">
      ```bash theme={null}
      # Create a new directory
      mkdir intuned-webhook-server && cd intuned-webhook-server

      # Initialize project and install dependencies
      npm init -y && npm pkg set type="module"
      npm install express
      npm install -D typescript @types/node @types/express

      # Initialize TypeScript
      npx tsc --init
      ```

      Create `server.ts`:

      ```typescript theme={null}
      import express from 'express';

      const app = express();
      app.use(express.json());

      // Add the webhook endpoint code from above here

      const PORT = process.env.PORT || 3000;
      app.listen(PORT, () => {
        console.log(`Webhook server running on port ${PORT}`);
      });
      ```

      Run the server:

      ```bash theme={null}
      npx tsx server.ts
      ```
    </Tab>

    <Tab title="Flask (Python)">
      ```bash theme={null}
      # Create a new directory
      mkdir intuned-webhook-server && cd intuned-webhook-server

      # Initialize project and install Flask
      uv init
      uv add flask
      ```

      Create `main.py`:

      ```python theme={null}
      from flask import Flask, request, jsonify
      from typing import Dict, Any
      import logging

      app = Flask(__name__)
      logging.basicConfig(level=logging.INFO)

      # Add the webhook endpoint code from above here

      if __name__ == '__main__':
          app.run(host='0.0.0.0', port=3000)
      ```

      Run the server:

      ```bash theme={null}
      uv run python main.py
      ```
    </Tab>
  </Tabs>
</Accordion>

#### Expose your endpoint (local testing only)

If you're testing locally, use a tunneling service to make your endpoint publicly accessible:

```bash theme={null}
# Using localtunnel (free)
npm install -g localtunnel
lt --port 3000

# Copy the HTTPS URL (e.g., https://hip-mugs-push.loca.lt)
```

<Note>
  Skip this step if your backend is already deployed and publicly accessible. For production, always use HTTPS endpoints.
</Note>

#### Test your endpoint

Verify your endpoint is working:

```bash theme={null}
curl -X POST https://your-webhook-url.com/webhooks/intuned \
  -H "Content-Type: application/json" \
  -d '{
    "workspaceId": "ws_test",
    "project": { "id": "proj_test", "name": "Test Project" },
    "projectJob": { "id": "job_test" },
    "projectJobRun": { "id": "jr_test" },
    "apiInfo": {
      "name": "extract_data",
      "parameters": { "url": "https://example.com" },
      "runId": "run_test",
      "result": {
        "status": "completed",
        "result": { "data": "Sample extracted data" }
      }
    }
  }'
```

You should see the test data logged in your server console.

### 2. Configure a Job with a webhook sink

<Steps>
  <Step title="Prepare a project" icon="book-open">
    You can use an existing project or create a new one.

    For this example, we'll use the `ecommerce-scraper-quickstart` project that you can deploy using the [Deploy your first scraper](/docs/main/00-getting-started/quickstarts/scraper) quickstart tutorial.
  </Step>

  <Step title="Create a Job with a webhook sink" icon="webhook">
    <Tabs>
      <Tab title="Dashboard">
        1. Go to [app.intuned.io](https://app.intuned.io)
        2. Open your `ecommerce-scraper-quickstart` project
        3. Select the **Jobs** tab
        4. Select Create Job
        5. Fill in the Job ID and payloads

        * Set Job ID to: `default-with-webhook`
        * Set payload api to `list` and empty parameter object `{}`

        6. Enable sink configuration and add your webhook details with the following fields:
           * **Type**: `webhook`
           * **URL**: Your webhook endpoint URL (e.g., `https://your-domain.com/webhooks/intuned`)
           * **Headers** (optional): Custom headers for authentication (e.g., `{"Authorization": "Bearer your-secret-token"}`)
           * **Retry** (optional): Number of retry attempts on failure (default: 3)
           * **Timeout** (optional): Request timeout in milliseconds (default: 5000)

        <Tip>
          Always use HTTPS endpoints in production to ensure data security. Store authentication tokens in environment variables rather than hardcoding them.
        </Tip>

        <Frame>
          <img alt="Job Sink Configuration" />
        </Frame>

        7. Select **Save** to create the Job.
      </Tab>

      <Tab title="TypeScript SDK">
        ```typescript theme={null}
        import { IntunedClient } from "@intuned/client";

        const intunedClient = new IntunedClient({
          workspaceId: "your-workspace-id",
          apiKey: process.env["INTUNED_API_KEY"] ?? "",
        });

        async function createJobWithWebhookSink() {
          const result = await intunedClient.projects.jobs.create(
            "ecommerce-scraper-quickstart",
            {
              id: "default-with-webhook",
              payload: [
                {
                  apiName: "list",
                  parameters: {},
                },
              ],
              configuration: {
                retry: {
                  maximumAttempts: 3,
                },
              },
              sink: {
                type: "webhook",
                url: "https://your-webhook-url.com/webhooks/intuned",
                headers: {
                  Authorization: "Bearer your-secret-token",
                },
              },
            }
          );

          console.log(result);
        }

        createJobWithWebhookSink();
        ```

        <Tip>
          Always use HTTPS endpoints in production to ensure data security. Store authentication tokens in environment variables rather than hardcoding them.
        </Tip>
      </Tab>

      <Tab title="Python SDK">
        ```python theme={null}
        from intuned_client import IntunedClient
        from intuned_client import models
        import os

        with IntunedClient(
            workspace_id="your-workspace-id",
            api_key=os.getenv("INTUNED_API_KEY", ""),
        ) as ic_client:
            result = ic_client.projects.jobs.create(
                project_name="ecommerce-scraper-quickstart",
                body=models.JobsCreateRequestBody(
                    id="default-with-webhook",
                    payload=[
                        {
                            "apiName": "list",
                            "parameters": {},
                        },
                    ],
                    configuration={
                        "retry": {
                            "maximumAttempts": 3,
                        },
                    },
                    sink={
                        "type": "webhook",
                        "url": "https://your-webhook-url.com/webhooks/intuned",
                        "headers": {
                            "Authorization": "Bearer your-secret-token",
                        },
                    },
                ),
            )

            print(result)
        ```

        <Tip>
          Always use HTTPS endpoints in production to ensure data security. Store authentication tokens in environment variables rather than hardcoding them.
        </Tip>
      </Tab>
    </Tabs>
  </Step>

  <Step title="Trigger the Job" icon="play">
    <Tabs>
      <Tab title="Dashboard">
        1. In the Jobs tab, find the Job you created
        2. Select **...** next to the Job
        3. Select **Trigger**

        The Job starts running immediately. You'll see the JobRun appear in the dashboard with status updates.
      </Tab>

      <Tab title="TypeScript SDK">
        ```typescript theme={null}
        import { IntunedClient } from "@intuned/client";

        const intunedClient = new IntunedClient({
            workspaceId: "your-workspace-id",
            apiKey: process.env["INTUNED_API_KEY"] ?? "",
        });

        async function triggerJob() {
            const result = await intunedClient.projects.jobs.trigger(
                "ecommerce-scraper-quickstart",
                "default-with-webhook"
            );

            console.log(`JobRun started: ${result.id}`);
            console.log(`Monitor at: https://app.intuned.io/workspaces/your-workspace-id/projects/ecommerce-scraper-quickstart/jobs/default-with-webhook/${result.id}`);
        }

        triggerJob();
        ```
      </Tab>

      <Tab title="Python SDK">
        ```python theme={null}
        from intuned_client import IntunedClient
        import os

        with IntunedClient(
            workspace_id="your-workspace-id",
            api_key=os.getenv("INTUNED_API_KEY", ""),
        ) as ic_client:
            result = ic_client.projects.jobs.trigger(
                project_name="ecommerce-scraper-quickstart",
                job_id="default-with-webhook"
            )

            print(f"JobRun started: {result.id}")
            print(f"Monitor at: https://app.intuned.io/workspaces/your-workspace-id/projects/ecommerce-scraper-quickstart/jobs/default-with-webhook/{result.id}")
        ```
      </Tab>
    </Tabs>
  </Step>
</Steps>

After triggering the Job:

1. **Job starts immediately** - Visible in Intuned dashboard
2. **API Runs execute** - The `list` API runs first, then `details` APIs for each product
3. **Webhooks deliver** - When each API Run completes, Intuned sends a webhook to your endpoint

<Tip>
  The ecommerce scraper uses `extendPayload` to create detail tasks for each discovered product. You'll receive multiple webhooks: one for the initial `list` Run, then one for each `details` Run as they complete.
</Tip>

<Note>
  If webhook delivery fails, Intuned retries with exponential backoff for up to 1 hour. Unlike S3/R2 sinks, webhook failures don't pause the Job; the Job continues and undelivered webhooks are skipped after all retries are exhausted. For detailed retry behavior, see [Sink Retry Behavior](/docs/main/05-references/sink-retry-behavior).
</Note>

Check your webhook endpoint logs - you should see payloads with this structure:

<Accordion title="View sample webhook payload">
  ```json theme={null}
  {
    "workspaceId": "e95cb8d1-f212-4c04-ace1-c0f77e8708c7",
    "apiInfo": {
        "name": "details",
        "runId": "656CxOdANRlR5lWUAt_eC",
        "parameters": {
            "detailsUrl": "https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie/",
            "name": "Abominable Hoodie"
        },
        "result": {
            "status": "completed",
            "result": [
              {
                "id": "prod-1",
                "name": "Wireless Headphones",
                "price": "$79.99"
              },
              {
                "id": "prod-2",
                "name": "Smart Watch",
                "price": "$199.99"
              }
            ],
            "statusCode": 200
        }
    },
    "project": {
        "id": "482bf507-5fcc-43ed-9443-d8fff86015c4",
        "name": "ecommerce-scraper-quickstart"
    },
    "projectJob": {
        "id": "default-with-webhook"
    },
    "projectJobRun": {
        "id": "08523ea6-5c6b-413e-995a-40e4f6fd7846"
    }
  }
  ```
</Accordion>

### Configuration options

For full details on webhook API schema and available configuration options, see the [API Reference](/docs/client-apis/api-reference/sinks/webhook).

Key configuration fields:

| Field        | Required | Description                                                                   |
| ------------ | -------- | ----------------------------------------------------------------------------- |
| `type`       | Yes      | Must be `"webhook"`                                                           |
| `url`        | Yes      | Your webhook endpoint URL (HTTPS recommended)                                 |
| `headers`    | No       | Custom headers for authentication (e.g., `{"Authorization": "Bearer token"}`) |
| `retry`      | No       | Number of retry attempts on failure (default: 3)                              |
| `timeout`    | No       | Request timeout in milliseconds (default: 5000)                               |
| `skipOnFail` | No       | Skip sending failed Runs to webhook (default: false)                          |
| `apisToSend` | No       | List of specific API names to send (default: all APIs)                        |

## Common data processing patterns

After receiving webhook data, you'll typically want to process it. Here are common patterns:

* **Store in a database**: Parse the extracted data and upsert records. Use the `runId` as a unique key to handle duplicates from webhook retries.
* **Validate and clean**: Check data structure and clean values before processing. Filter out malformed records and log validation errors for debugging.
* **Send to monitoring**: Route failures to error tracking services (Sentry, Datadog) for debugging. Track metrics like success rates and processing times.
* **Trigger downstream workflows**: Use webhook data to kick off additional processes—update other services, publish to message queues, or send notifications based on the results.

## Best practices

* **Respond quickly**: Return a 200 status within 5 seconds. Process data asynchronously after responding to prevent Intuned from timing out and retrying.
* **Secure your endpoint**: Use HTTPS in production. Verify the `Authorization` header matches your secret and validate payload structure before processing.
* **Handle idempotency**: Webhook deliveries may be retried. Use the `runId` field as a deduplication key to avoid processing the same Run twice.
* **Monitor webhook health**: Log received webhooks with timestamps, alert if none arrive within expected schedule, and track processing error rates.

## Troubleshooting

### Webhooks not being received

**Cause:** Endpoint URL is incorrect, not publicly accessible, or blocked by firewall/SSL issues.

**Solution:** Test endpoint accessibility with `curl` from an external server. Check server logs for incoming requests and verify the webhook URL in your Intuned Job configuration.

### Webhook timeout errors

**Cause:** Your endpoint takes too long to respond (>5 seconds).

**Solution:** Return 200 status immediately, then process data asynchronously using background jobs or queues.

### Authentication failures

**Cause:** Authorization header mismatch between Job configuration and endpoint validation.

**Solution:** Verify the header name and value in your Job configuration. Check for extra spaces or encoding issues in the secret token.

### Duplicate webhook deliveries

**Cause:** Intuned retries on slow responses or network issues.

**Solution:** Implement idempotency using the `runId` field from the payload as a deduplication key.

## Related resources

<CardGroup>
  <Card title="Sinks overview" icon="code" href="/client-apis/api-reference/sinks/overview">
    Complete API documentation for creating and managing Jobs
  </Card>

  <Card title="Jobs" icon="play" href="/main/02-features/jobs-batched-executions">
    Learn more about executing and monitoring JobRuns
  </Card>

  <Card title="How to QA your scrapers" icon="clock" href="/main/03-how-to/solve/qa-automation-results">
    Best practices for validating and ensuring data quality
  </Card>

  <Card title="Runs & Single Executions" icon="database" href="/main/02-features/runs-single-executions">
    Learn more about executing and monitoring single API Runs
  </Card>
</CardGroup>


# Configuration
Source: https://intunedhq.com/docs/main/05-references/cli/configuration



## Settings file

The CLI reads project settings from a settings file in the root of your project directory. The following file names are supported (checked in order):

* `Intuned.json`
* `Intuned.jsonc`

You can use the `--settings-format` option on supported commands to specify the format when creating or modifying the settings file.

For the full settings file schema (including the `defaults` block for proxy, timeouts, traces, retries, and auth session behavior), see [Intuned.json reference](/docs/main/05-references/intuned-json).

***

## Environment variables

| Variable                         | Description                                                                                                                                  |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `INTUNED_API_KEY`                | API key for authentication. When set, takes priority over browser-based login. Requires `INTUNED_WORKSPACE_ID` or `workspaceId` in settings. |
| `INTUNED_WORKSPACE_ID`           | Workspace ID. Required when using API key authentication if not set in the settings file.                                                    |
| `INTUNED_CHROMIUM_PATH`          | Path to a custom Chromium executable for the dev browser.                                                                                    |
| `INTUNED_STEALTH_CHROMIUM_PATH`  | Path to a Chromium executable used for stealth mode.                                                                                         |
| `INTUNED_SETTINGS_PATH`          | Directory for CLI settings and credentials. Defaults to `~/.intuned`.                                                                        |
| `INTUNED_EXTENSION_PATH`         | Path to the captcha solver browser extension.                                                                                                |
| `INTUNED_CAPTCHA_EXTENSION_PORT` | Port for the captcha solver extension to communicate on.                                                                                     |

***

## Authentication

The CLI supports two authentication methods, resolved in the following priority order:

1. **API key** (`INTUNED_API_KEY` environment variable) — authenticates using an API key. Requires `INTUNED_WORKSPACE_ID` or `workspaceId` in the settings file. The API key is exchanged for a short-lived token that is cached and auto-refreshed.

2. **Browser-based login** (`intuned auth login`) — interactive browser-based OAuth login. Credentials are stored in `~/.intuned/credentials.json` and auto-refreshed.

If neither method is configured, the CLI returns an authentication error.

***

## Credentials storage

Browser-based login credentials are stored in:

```
~/.intuned/credentials.json
```

This file is managed automatically by `intuned auth login` and `intuned auth logout`. Tokens are auto-refreshed when they expire.

## Reference

* [Overview](./overview)
* [Dev reference](./dev) — Develop and test automations locally.
* [Platform reference](./platform) — Manage projects on Intuned platform.


# Development
Source: https://intunedhq.com/docs/main/05-references/cli/dev



The Intuned CLI provides development commands to develop, test, and deploy automations locally.

## Commands

* [Init](#init)
* [List Templates](#list-templates)
* Run
  * [Run API](#run-api)
  * [Run AuthSession Create](#run-authsession-create)
  * [Run AuthSession Validate](#run-authsession-validate)
  * [Run AuthSession Update](#run-authsession-update)
* Attempt
  * [Attempt API](#attempt-api)
  * [Attempt AuthSession Check](#attempt-authsession-check)
  * [Attempt AuthSession Create](#attempt-authsession-create)
* Test Job
  * [Test Job Trigger](#testjob-trigger)
  * [Test Job Result](#testjob-result)
  * [Test Job Download](#testjob-download)
  * [Test Job Terminate](#testjob-terminate)
* [Provision](#provision)
* [Deploy](#deploy)
* Browser
  * [Browser Start](#browser-start)
  * [Browser Stop](#browser-stop)
  * [Browser Status](#browser-status)
  * [Browser Tabs Create](#browser-tabs-create)
  * [Browser Tabs List](#browser-tabs-list)
  * [Browser Tabs Close](#browser-tabs-close)
* Stealth
  * [Stealth Enable](#stealth-enable)
  * [Stealth Disable](#stealth-disable)
* Captcha Solve
  * [Captcha Solve Enable](#captcha-solve-enable)
  * [Captcha Solve Disable](#captcha-solve-disable)
* Proxy
  * [Proxy Set](#proxy-set)
  * [Proxy Clear](#proxy-clear)

***

## Init

Initialize a new Intuned project from a template or GitHub URL.

```txt theme={null}
intuned dev init [directory] [options]
```

<ParamField type="string">
  Directory to initialize the project in. Defaults to the current directory.
</ParamField>

**Options**

```txt theme={null}
    -t, --template <id>
    -l, --language <language>
    --from-github <url>
    --overwrite
    --non-interactive
    --stealth
    --captcha-solve
    --proxy [url]
    --proxy-deployed
    --install-deps
    --settings-format <format>
    -h, --help
```

<ParamField type="string">
  Template ID to use for project initialization. Use `dev list-templates` to see available templates.
</ParamField>

<ParamField type="string">
  Project language. Supported values: `typescript`, `python`.
</ParamField>

<ParamField type="string">
  Clone from a GitHub URL instead of using a template.
</ParamField>

<ParamField>
  Overwrite existing files instead of erroring on conflicts.
</ParamField>

<ParamField>
  Run without interactive prompts.
</ParamField>

<ParamField>
  Enable stealth mode in the project settings.
</ParamField>

<ParamField>
  Enable captcha solver in the project settings.
</ParamField>

<ParamField type="string">
  Set the dev proxy in the project settings. Pass alone to opt into `intuned://auto` (Intuned's managed proxy) when no default proxy is already set, or pass an explicit URL.
</ParamField>

<ParamField>
  Also set the deployed environment proxy to the same URL. Requires `--proxy`.
</ParamField>

<ParamField>
  Install dependencies after project creation.
</ParamField>

<ParamField type="string">
  Format of the settings file. Supported values: `json`, `jsonc`, `yaml`, `toml`. If not specified, auto-detected from the existing file or defaults to `jsonc`.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## List Templates

List available project templates.

```txt theme={null}
intuned dev list-templates [options]
```

**Options**

```txt theme={null}
    -l, --language <language>
    --json
    -h, --help
```

<ParamField type="string">
  Filter templates by language. Supported values: `typescript`, `python`.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Run API

Execute an API Run.

```txt theme={null}
intuned dev run api <api-name> <parameters> [options]
```

<ParamField type="string">
  Name of the API to run. APIs are under the `api` directory, without the file extension. Use `/` for nested directories.
</ParamField>

<ParamField type="string">
  Parameters to pass to the API. Accepts a JSON string or a path to a JSON file.
</ParamField>

**Options**

```txt theme={null}
    --retries <number>
    --auth-session <id>
    --auto-recreate
    --auth-check-retries <number>
    --auth-create-retries <number>
    -o, --output-file <path>
    --proxy <url>
    --timezone <tz>
    --timeout <time>
    --start-to-end-timeout <time>
    --headless
    --trace
    --traces-path <path>
    --auth-session-instances-path <path>
    --keep-browser-open
    --cdp-url <url>
    --cdp-browser-name <name>
    --cdp-tab-id <tab_id>
    -h, --help
```

<ParamField type="int">
  Number of retries for the API if an error in the API execution occurs.
</ParamField>

<ParamField type="string">
  Auth session ID to use for the run. The auth session files are expected to be in `./auth-sessions-instances/<id>`.

  Must be provided if auth sessions are enabled in Intuned.json. Ignored if not.
</ParamField>

<ParamField>
  Auto-recreate the auth session if validation fails.

  Only applicable if auth sessions are enabled in Intuned.json.
</ParamField>

<ParamField type="int">
  Number of auth session check retries.

  Only applicable if auth sessions are enabled in Intuned.json.
</ParamField>

<ParamField type="int">
  Number of auth session create retries (for auto-recreate).

  Only applicable if auth sessions are enabled in Intuned.json and `--auto-recreate` is set.
</ParamField>

<ParamField type="string">
  Write the run result to a file instead of stdout. The output is a JSON object containing the result and extended payloads (if applicable).
</ParamField>

<ParamField type="string">
  Proxy URL to use for the browser.
</ParamField>

<ParamField type="string">
  IANA timezone to apply to the browser (for example `America/New_York`). See [Proxy options](/docs/main/02-features/stealth-mode-captcha-solving-proxies#proxy-options).
</ParamField>

<ParamField type="string">
  Timeout for each attempt. Accepts milliseconds (integer) or an [ms-formatted string](https://www.npmjs.com/package/ms) (e.g., `30s`, `5m`, `1h`).
</ParamField>

<ParamField type="string">
  Overall timeout from start to end of the command. Accepts milliseconds or an ms-formatted string.
</ParamField>

<ParamField>
  Run the browser in headless mode. By default, the browser runs in headful mode (a visible window).
</ParamField>

<ParamField>
  Capture a Playwright trace of each attempt. Useful for debugging. Traces are saved to `./traces/` by default.
</ParamField>

<ParamField type="string">
  Custom directory to save trace files. Defaults to `traces/`.
</ParamField>

<ParamField type="string">
  Custom directory to store/load auth session instance files. Defaults to `auth-sessions-instances/`.
</ParamField>

<ParamField>
  Keep the last browser open after the command completes. Useful for debugging.
</ParamField>

<ParamField type="string">
  Chrome DevTools Protocol URL to connect to an existing browser instance. When set, `--proxy`, `--headless`, and `--keep-browser-open` options are ignored.
</ParamField>

<ParamField type="string">
  Name of a persistent browser instance (started with `dev browser start`) to connect to via CDP.
</ParamField>

<ParamField type="string">
  Browser tab ID to execute on. Requires `--cdp-url` or `--cdp-browser-name`. If not specified, the first tab is used.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Run AuthSession Create

Executes an AuthSession:Create Run. Auth sessions must be enabled in Intuned.json.

```txt theme={null}
intuned dev run authsession create <parameters> [options]
```

<ParamField type="string">
  Parameters for auth session creation. Accepts a JSON string or a path to a JSON file.
</ParamField>

**Options**

```txt theme={null}
    --id <id>
    --check-retries <number>
    --create-retries <number>
    --proxy <url>
    --timezone <tz>
    --timeout <time>
    --start-to-end-timeout <time>
    --headless
    --trace
    --traces-path <path>
    --auth-session-instances-path <path>
    --keep-browser-open
    --cdp-url <url>
    --cdp-browser-name <name>
    --cdp-tab-id <tab_id>
    -h, --help
```

<ParamField type="string">
  ID of the auth session to create. The auth session files will be created in `./auth-sessions-instances/<id>`. Auto-generated if not provided.
</ParamField>

<ParamField type="int">
  Number of attempts to check if the auth session is valid after successful creation.
</ParamField>

<ParamField type="int">
  Number of attempts to create the auth session.
</ParamField>

<ParamField type="string">
  Proxy URL to use for the browser.
</ParamField>

<ParamField type="string">
  IANA timezone to apply to the browser (for example `America/New_York`). See [Proxy options](/docs/main/02-features/stealth-mode-captcha-solving-proxies#proxy-options).
</ParamField>

<ParamField type="string">
  Timeout for each attempt. Accepts milliseconds (integer) or an [ms-formatted string](https://www.npmjs.com/package/ms) (e.g., `30s`, `5m`, `1h`).
</ParamField>

<ParamField type="string">
  Overall timeout from start to end of the command. Accepts milliseconds or an ms-formatted string.
</ParamField>

<ParamField>
  Run the browser in headless mode. By default, the browser runs in headful mode (a visible window).
</ParamField>

<ParamField>
  Capture a Playwright trace of each attempt. Useful for debugging. Traces are saved to `./traces/` by default.
</ParamField>

<ParamField type="string">
  Custom directory to save trace files. Defaults to `traces/`.
</ParamField>

<ParamField type="string">
  Custom directory to store/load auth session instance files. Defaults to `auth-sessions-instances/`.
</ParamField>

<ParamField>
  Keep the last browser open after the command completes. Useful for debugging.
</ParamField>

<ParamField type="string">
  Chrome DevTools Protocol URL to connect to an existing browser instance. When set, `--proxy`, `--headless`, and `--keep-browser-open` options are ignored.
</ParamField>

<ParamField type="string">
  Name of a persistent browser instance (started with `dev browser start`) to connect to via CDP.
</ParamField>

<ParamField type="string">
  Browser tab ID to execute on. Requires `--cdp-url` or `--cdp-browser-name`. If not specified, the first tab is used.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Run AuthSession Validate

Executes an AuthSession:Validate Run. Auth sessions must be enabled in Intuned.json.

```txt theme={null}
intuned dev run authsession validate <id> [options]
```

<ParamField type="string">
  ID of the auth session to validate. The auth session files are expected to be in `./auth-sessions-instances/<id>`.
</ParamField>

**Options**

```txt theme={null}
    --auto-recreate
    --check-retries <number>
    --create-retries <number>
    --proxy <url>
    --timezone <tz>
    --timeout <time>
    --start-to-end-timeout <time>
    --headless
    --trace
    --traces-path <path>
    --auth-session-instances-path <path>
    --keep-browser-open
    --cdp-url <url>
    --cdp-browser-name <name>
    --cdp-tab-id <tab_id>
    -h, --help
```

<ParamField>
  Auto-recreate the auth session if validation fails.
</ParamField>

<ParamField type="int">
  Number of attempts to check if the auth session is valid.
</ParamField>

<ParamField type="int">
  Number of attempts to create the auth session if initial check fails.

  Only applicable if `--auto-recreate` is set.
</ParamField>

<ParamField type="string">
  Proxy URL to use for the browser.
</ParamField>

<ParamField type="string">
  IANA timezone to apply to the browser (for example `America/New_York`). See [Proxy options](/docs/main/02-features/stealth-mode-captcha-solving-proxies#proxy-options).
</ParamField>

<ParamField type="string">
  Timeout for each attempt. Accepts milliseconds (integer) or an [ms-formatted string](https://www.npmjs.com/package/ms) (e.g., `30s`, `5m`, `1h`).
</ParamField>

<ParamField type="string">
  Overall timeout from start to end of the command. Accepts milliseconds or an ms-formatted string.
</ParamField>

<ParamField>
  Run the browser in headless mode. By default, the browser runs in headful mode (a visible window).
</ParamField>

<ParamField>
  Capture a Playwright trace of each attempt. Useful for debugging. Traces are saved to `./traces/` by default.
</ParamField>

<ParamField type="string">
  Custom directory to save trace files. Defaults to `traces/`.
</ParamField>

<ParamField type="string">
  Custom directory to store/load auth session instance files. Defaults to `auth-sessions-instances/`.
</ParamField>

<ParamField>
  Keep the last browser open after the command completes. Useful for debugging.
</ParamField>

<ParamField type="string">
  Chrome DevTools Protocol URL to connect to an existing browser instance. When set, `--proxy`, `--headless`, and `--keep-browser-open` options are ignored.
</ParamField>

<ParamField type="string">
  Name of a persistent browser instance (started with `dev browser start`) to connect to via CDP.
</ParamField>

<ParamField type="string">
  Browser tab ID to execute on. Requires `--cdp-url` or `--cdp-browser-name`. If not specified, the first tab is used.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Run AuthSession Update

Executes an AuthSession:Update Run. Auth sessions must be enabled in Intuned.json.

```txt theme={null}
intuned dev run authsession update <id> [options]
```

<ParamField type="string">
  ID of the auth session to update. The auth session files are expected to be in `./auth-sessions-instances/<id>`.
</ParamField>

**Options**

```txt theme={null}
    --input <parameters>
    --check-retries <number>
    --create-retries <number>
    --proxy <url>
    --timezone <tz>
    --timeout <time>
    --start-to-end-timeout <time>
    --headless
    --trace
    --traces-path <path>
    --auth-session-instances-path <path>
    --keep-browser-open
    --cdp-url <url>
    --cdp-browser-name <name>
    --cdp-tab-id <tab_id>
    -h, --help
```

<ParamField type="string">
  New JSON parameters or path to a JSON file. If not provided, the last used parameters will be used.
</ParamField>

<ParamField type="int">
  Number of attempts to check if the auth session is valid after successful update.
</ParamField>

<ParamField type="int">
  Number of attempts to create the auth session.
</ParamField>

<ParamField type="string">
  Proxy URL to use for the browser.
</ParamField>

<ParamField type="string">
  IANA timezone to apply to the browser (for example `America/New_York`). See [Proxy options](/docs/main/02-features/stealth-mode-captcha-solving-proxies#proxy-options).
</ParamField>

<ParamField type="string">
  Timeout for each attempt. Accepts milliseconds (integer) or an [ms-formatted string](https://www.npmjs.com/package/ms) (e.g., `30s`, `5m`, `1h`).
</ParamField>

<ParamField type="string">
  Overall timeout from start to end of the command. Accepts milliseconds or an ms-formatted string.
</ParamField>

<ParamField>
  Run the browser in headless mode. By default, the browser runs in headful mode (a visible window).
</ParamField>

<ParamField>
  Capture a Playwright trace of each attempt. Useful for debugging. Traces are saved to `./traces/` by default.
</ParamField>

<ParamField type="string">
  Custom directory to save trace files. Defaults to `traces/`.
</ParamField>

<ParamField type="string">
  Custom directory to store/load auth session instance files. Defaults to `auth-sessions-instances/`.
</ParamField>

<ParamField>
  Keep the last browser open after the command completes. Useful for debugging.
</ParamField>

<ParamField type="string">
  Chrome DevTools Protocol URL to connect to an existing browser instance. When set, `--proxy`, `--headless`, and `--keep-browser-open` options are ignored.
</ParamField>

<ParamField type="string">
  Name of a persistent browser instance (started with `dev browser start`) to connect to via CDP.
</ParamField>

<ParamField type="string">
  Browser tab ID to execute on. Requires `--cdp-url` or `--cdp-browser-name`. If not specified, the first tab is used.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Attempt API

Execute an API Attempt.

```txt theme={null}
intuned dev attempt api <api-name> <parameters> [options]
```

<ParamField type="string">
  Name of the API to execute. APIs are under the `api` directory, without the file extension. Use `/` for nested directories.
</ParamField>

<ParamField type="string">
  Parameters to pass to the API. Accepts a JSON string or a path to a JSON file.
</ParamField>

**Options**

```txt theme={null}
    --auth-session <id>
    -o, --output-file <path>
    --proxy <url>
    --timezone <tz>
    --timeout <time>
    --start-to-end-timeout <time>
    --headless
    --trace
    --traces-path <path>
    --auth-session-instances-path <path>
    --keep-browser-open
    --cdp-url <url>
    --cdp-browser-name <name>
    --cdp-tab-id <tab_id>
    -h, --help
```

<ParamField type="string">
  Auth session ID to use for the attempt. The auth session files are expected to be in `./auth-sessions-instances/<id>`.

  Must be provided if auth sessions are enabled in Intuned.json. Ignored if not.
</ParamField>

<ParamField type="string">
  Write the attempt result to a file instead of stdout. The output is a JSON object containing the result and extended payloads (if applicable).
</ParamField>

<ParamField type="string">
  Proxy URL to use for the browser.
</ParamField>

<ParamField type="string">
  IANA timezone to apply to the browser (for example `America/New_York`). See [Proxy options](/docs/main/02-features/stealth-mode-captcha-solving-proxies#proxy-options).
</ParamField>

<ParamField type="string">
  Timeout for each attempt. Accepts milliseconds (integer) or an [ms-formatted string](https://www.npmjs.com/package/ms) (e.g., `30s`, `5m`, `1h`).
</ParamField>

<ParamField type="string">
  Overall timeout from start to end of the command. Accepts milliseconds or an ms-formatted string.
</ParamField>

<ParamField>
  Run the browser in headless mode. By default, the browser runs in headful mode (a visible window).
</ParamField>

<ParamField>
  Capture a Playwright trace of each attempt. Useful for debugging. Traces are saved to `./traces/` by default.
</ParamField>

<ParamField type="string">
  Custom directory to save trace files. Defaults to `traces/`.
</ParamField>

<ParamField type="string">
  Custom directory to store/load auth session instance files. Defaults to `auth-sessions-instances/`.
</ParamField>

<ParamField>
  Keep the last browser open after the command completes. Useful for debugging.
</ParamField>

<ParamField type="string">
  Chrome DevTools Protocol URL to connect to an existing browser instance. When set, `--proxy`, `--headless`, and `--keep-browser-open` options are ignored.
</ParamField>

<ParamField type="string">
  Name of a persistent browser instance (started with `dev browser start`) to connect to via CDP.
</ParamField>

<ParamField type="string">
  Browser tab ID to execute on. Requires `--cdp-url` or `--cdp-browser-name`. If not specified, the first tab is used.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Attempt AuthSession Check

Executes an AuthSession:Check Attempt. Auth sessions must be enabled in Intuned.json.

```txt theme={null}
intuned dev attempt authsession check <id> [options]
```

<ParamField type="string">
  ID of the auth session to check. The auth session files are expected to be in `./auth-sessions-instances/<id>`.
</ParamField>

**Options**

```txt theme={null}
    --proxy <url>
    --timezone <tz>
    --timeout <time>
    --start-to-end-timeout <time>
    --headless
    --trace
    --traces-path <path>
    --auth-session-instances-path <path>
    --keep-browser-open
    --cdp-url <url>
    --cdp-browser-name <name>
    --cdp-tab-id <tab_id>
    -h, --help
```

See [Shared Dev Options](#shared-dev-options) for option details.

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Attempt AuthSession Create

Executes an AuthSession:Create Attempt. Auth sessions must be enabled in Intuned.json.

```txt theme={null}
intuned dev attempt authsession create <parameters> [options]
```

<ParamField type="string">
  Parameters for auth session creation. Accepts a JSON string or a path to a JSON file.
</ParamField>

**Options**

```txt theme={null}
    --id <id>
    --proxy <url>
    --timezone <tz>
    --timeout <time>
    --start-to-end-timeout <time>
    --headless
    --trace
    --traces-path <path>
    --auth-session-instances-path <path>
    --keep-browser-open
    --cdp-url <url>
    --cdp-browser-name <name>
    --cdp-tab-id <tab_id>
    -h, --help
```

<ParamField type="string">
  ID of the auth session to create. The auth session files will be created in `./auth-sessions-instances/<id>`. Auto-generated if not provided.
</ParamField>

<ParamField type="string">
  Proxy URL to use for the browser.
</ParamField>

<ParamField type="string">
  IANA timezone to apply to the browser (for example `America/New_York`). See [Proxy options](/docs/main/02-features/stealth-mode-captcha-solving-proxies#proxy-options).
</ParamField>

<ParamField type="string">
  Timeout for each attempt. Accepts milliseconds (integer) or an [ms-formatted string](https://www.npmjs.com/package/ms) (e.g., `30s`, `5m`, `1h`).
</ParamField>

<ParamField type="string">
  Overall timeout from start to end of the command. Accepts milliseconds or an ms-formatted string.
</ParamField>

<ParamField>
  Run the browser in headless mode. By default, the browser runs in headful mode (a visible window).
</ParamField>

<ParamField>
  Capture a Playwright trace of each attempt. Useful for debugging. Traces are saved to `./traces/` by default.
</ParamField>

<ParamField type="string">
  Custom directory to save trace files. Defaults to `traces/`.
</ParamField>

<ParamField type="string">
  Custom directory to store/load auth session instance files. Defaults to `auth-sessions-instances/`.
</ParamField>

<ParamField>
  Keep the last browser open after the command completes. Useful for debugging.
</ParamField>

<ParamField type="string">
  Chrome DevTools Protocol URL to connect to an existing browser instance. When set, `--proxy`, `--headless`, and `--keep-browser-open` options are ignored.
</ParamField>

<ParamField type="string">
  Name of a persistent browser instance (started with `dev browser start`) to connect to via CDP.
</ParamField>

<ParamField type="string">
  Browser tab ID to execute on. Requires `--cdp-url` or `--cdp-browser-name`. If not specified, the first tab is used.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## TestJob Trigger

Trigger a test job run with code from the current directory. Uploads project code and triggers execution on the Intuned platform.

```txt theme={null}
intuned dev test-job trigger [payloads] [options]
```

<ParamField type="string">
  JSON array of payloads. Each payload object should contain `apiName` (required), `parameters` (optional), and `retry` (optional). Accepts a JSON string or a path to a JSON file.

  Not required if `--from-job-config` is provided.
</ParamField>

**Options**

```txt theme={null}
    --from-job-config <path>
    --auth-session-input <path>
    --max-concurrent-requests <n>
    --retries <n>
    --request-timeout <n>
    --json
    -q, --quiet
    -h, --help
```

<ParamField type="string">
  Path to a job config file containing `payload` and optional `configuration` fields. Cannot be used together with the positional `payloads` argument.
</ParamField>

<ParamField type="string">
  Path to a JSON file with auth session input. Required if auth sessions are enabled in Intuned.json.
</ParamField>

<ParamField type="int">
  Maximum number of concurrent requests. Only applicable when using positional payloads (not `--from-job-config`).
</ParamField>

<ParamField type="int">
  Maximum retry attempts per payload. Only applicable when using positional payloads.
</ParamField>

<ParamField type="int">
  Request timeout in seconds. Only applicable when using positional payloads.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Suppress non-essential output.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## TestJob Result

Get the result of a test job run.

```txt theme={null}
intuned dev test-job result <run-id> [options]
```

<ParamField type="string">
  The run ID returned by `test-job trigger`.
</ParamField>

**Options**

```txt theme={null}
    -w, --wait-for <duration>
    --json
    -q, --quiet
    -h, --help
```

<ParamField type="string">
  Wait for the test job to complete, polling until the given duration expires. Accepts an [ms-formatted string](https://www.npmjs.com/package/ms) (e.g., `30s`, `5m`, `2h`).
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Suppress non-essential output.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## TestJob Download

Download test job results to a file.

```txt theme={null}
intuned dev test-job download <run-id> [options]
```

<ParamField type="string">
  The run ID returned by `test-job trigger`.
</ParamField>

**Options**

```txt theme={null}
    -o, --output-file <path>
    -w, --wait-for <duration>
    -q, --quiet
    -h, --help
```

<ParamField type="string">
  Output file path. Defaults to `test_results_<run-id>.json` in the current directory. Can be a directory path (file will be auto-named inside it).
</ParamField>

<ParamField type="string">
  Wait for the test job to complete before downloading. Accepts an ms-formatted string (e.g., `30s`, `5m`).
</ParamField>

<ParamField>
  Suppress non-essential output.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## TestJob Terminate

Terminate a running test job execution.

```txt theme={null}
intuned dev test-job terminate <run-id> [options]
```

<ParamField type="string">
  The run ID of the test execution to terminate.
</ParamField>

**Options**

```txt theme={null}
    --json
    -q, --quiet
    -h, --help
```

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Suppress non-essential output.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Project name resolution

Checkout, provision, save, and deploy use `--project-name` when provided. Otherwise, they use `projectName` from the Intuned settings file. The command fails if neither value is available.

***

## Checkout

Check out an existing Intuned project into a local directory. The CLI preserves local-only files. If an incoming file differs from a local file, the command asks before replacing it.

```txt theme={null}
intuned dev checkout [path] [options]
```

**Options**

```txt theme={null}
    -p, --project-name <name>
    --override
    --non-interactive
    --json
    -h, --help
```

<ParamField type="string">
  Directory where the project is checked out.
</ParamField>

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField>
  Replace local files that differ from the project on Intuned.
</ParamField>

<ParamField>
  Disable prompts. The command aborts on conflicting files unless you also pass `--override`.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Provision

Create an empty project on Intuned through the CLI. The command updates the local project settings, `.env`, and `.intuned/checkout`, then offers to save the local files. It fails if the project already exists and recommends `intuned dev save` instead.

```txt theme={null}
intuned dev provision [options]
```

**Options**

```txt theme={null}
    -p, --project-name <name>
    --non-interactive
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField>
  Disable prompts. The project is created without saving local files.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Save

Provision the project if it does not exist, then save local project files to Intuned. By default, the command blocks if the project changed on Intuned after the local checkout or if local and Intuned changes conflict.

```txt theme={null}
intuned dev save [options]
```

**Options**

```txt theme={null}
    -p, --project-name <name>
    --on-stale <action>
    --on-conflict <action>
    --non-interactive
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField type="string">
  Action when the local checkout is stale. Use `block` to stop without changing the project, or `merge` to merge non-conflicting local and Intuned changes. After a stale merge, check out the project again to update the local files.
</ParamField>

<ParamField type="string">
  Action when local and Intuned changes conflict. Use `block` to stop without changing the project, or `override` to use local content for conflicting files. `override` also permits a full save when checkout information is missing or invalid.
</ParamField>

<ParamField>
  Disable prompts.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Deploy

Save and deploy the project to Intuned. The deployment starts only after the save succeeds, unless you pass `--no-save`.

```txt theme={null}
intuned dev deploy [options]
```

**Options**

```txt theme={null}
    -p, --project-name <name>
    --no-save
    --on-stale <action>
    --on-conflict <action>
    --non-interactive
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField>
  Skip saving local changes before deployment.
</ParamField>

<ParamField type="string">
  Pass the `block` or `merge` stale-checkout policy to the save step.
</ParamField>

<ParamField type="string">
  Pass the `block` or `override` conflict policy to the save step.
</ParamField>

<ParamField>
  Disable prompts. Stale checkouts and conflicts still block unless their explicit policy allows the save.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

## Browser Start

Start a persistent browser instance managed by the CLI.

```txt theme={null}
intuned dev browser start [options]
```

**Options**

```txt theme={null}
    -n, --name <name>
    --cdp-port <port>
    --headless
    --viewport <WxH>
    --proxy <url>
    --timezone <tz>
    --json
    -h, --help
```

<ParamField type="string">
  Name for the browser instance. Used to reference the browser in other commands.
</ParamField>

<ParamField type="int">
  CDP port for the browser. If not specified, an available port is auto-selected (starting from 9222).
</ParamField>

<ParamField>
  Run the browser in headless mode.
</ParamField>

<ParamField type="string">
  Viewport size in `WIDTHxHEIGHT` format (e.g., `1920x1080`).
</ParamField>

<ParamField type="string">
  Proxy URL to use for the browser. Falls back to `defaults.proxy.dev` from the Intuned settings file when omitted.
</ParamField>

<ParamField type="string">
  IANA timezone to apply to the browser (for example `America/New_York`). Falls back to `defaults.timezone.dev` when omitted. See [Proxy options](/docs/main/02-features/stealth-mode-captcha-solving-proxies#proxy-options).
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Browser Stop

Stop persistent browser instance(s).

```txt theme={null}
intuned dev browser stop [options]
```

**Options**

```txt theme={null}
    -n, --name <name>
    -a, --all
    --json
    -h, --help
```

<ParamField type="string">
  Name of the browser instance to stop.
</ParamField>

<ParamField>
  Stop all running browser instances.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Browser Status

Show persistent browser instance status.

```txt theme={null}
intuned dev browser status [options]
```

**Options**

```txt theme={null}
    -n, --name <name>
    --json
    -h, --help
```

<ParamField type="string">
  Browser instance name. If not specified, shows status for all instances.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Browser Tabs Create

Create a new tab in a persistent browser instance.

```txt theme={null}
intuned dev browser tabs create [options]
```

**Options**

```txt theme={null}
    -n, --name <name>
    -u, --url <url>
    --json
    -h, --help
```

<ParamField type="string">
  Browser instance name.
</ParamField>

<ParamField type="string">
  Initial URL for the new tab.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Browser Tabs List

List all tabs in a persistent browser instance.

```txt theme={null}
intuned dev browser tabs list [options]
```

**Options**

```txt theme={null}
    -n, --name <name>
    --json
    -h, --help
```

<ParamField type="string">
  Browser instance name.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Browser Tabs Close

Close a tab by its ID in a persistent browser instance.

```txt theme={null}
intuned dev browser tabs close <tab-id> [options]
```

<ParamField type="string">
  Tab ID to close (4-character short ID). Use `dev browser tabs list` to find tab IDs.
</ParamField>

**Options**

```txt theme={null}
    -n, --name <name>
    --json
    -h, --help
```

<ParamField type="string">
  Browser instance name.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Stealth Enable

Enable stealth mode, restarting the browser with stealth mode on if it is running.

```txt theme={null}
intuned dev stealth enable [options]
```

**Options**

```txt theme={null}
    -n, --name <name>
    --settings-format <format>
    --json
    -h, --help
```

<ParamField type="string">
  Browser instance name.
</ParamField>

<ParamField type="string">
  Format of the settings file. Supported values: `json`, `jsonc`, `yaml`, `toml`. If not specified, auto-detected from the existing file or defaults to `jsonc`.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Stealth Disable

Disable stealth mode, restarting the browser if it is running.

```txt theme={null}
intuned dev stealth disable [options]
```

**Options**

```txt theme={null}
    -n, --name <name>
    --json
    -h, --help
```

<ParamField type="string">
  Browser instance name.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Captcha Solve Enable

Enable the captcha solver extension, restarting the browser if it is running.

```txt theme={null}
intuned dev captcha-solve enable [options]
```

**Options**

```txt theme={null}
    -n, --name <name>
    --settings-format <format>
    --json
    -h, --help
```

<ParamField type="string">
  Browser instance name.
</ParamField>

<ParamField type="string">
  Format of the settings file. Supported values: `json`, `jsonc`, `yaml`, `toml`. If not specified, auto-detected from the existing file or defaults to `jsonc`.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Captcha Solve Disable

Disable the captcha solver extension, restarting the browser if it is running.

```txt theme={null}
intuned dev captcha-solve disable [options]
```

**Options**

```txt theme={null}
    -n, --name <name>
    --json
    -h, --help
```

<ParamField type="string">
  Browser instance name.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Proxy Set

Set the dev proxy, restarting the browser if it is running. Optionally set the browser timezone for the same environment.

```txt theme={null}
intuned dev proxy set [proxy-url] [options]
```

<ParamField type="string">
  Proxy URL to set. Defaults to `intuned://auto`, Intuned's managed proxy.
</ParamField>

**Options**

```txt theme={null}
    -n, --name <name>
    --no-browser-restart
    --deployed
    --timezone <tz>
    --settings-format <format>
    --json
    -h, --help
```

<ParamField type="string">
  Browser instance name.
</ParamField>

<ParamField>
  Do not restart the browser after setting the proxy.
</ParamField>

<ParamField>
  Also set the deployed proxy in addition to the dev proxy.
</ParamField>

<ParamField type="string">
  IANA timezone to set alongside the proxy (for example `America/New_York`). Stored under `defaults.timezone` in the Intuned settings file. See [Proxy options](/docs/main/02-features/stealth-mode-captcha-solving-proxies#proxy-options).
</ParamField>

<ParamField type="string">
  Format of the settings file. Supported values: `json`, `jsonc`, `yaml`, `toml`. If not specified, auto-detected from the existing file or defaults to `jsonc`.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Proxy Clear

Clear the dev and deployed proxy, restarting the browser if it is running.

```txt theme={null}
intuned dev proxy clear [options]
```

**Options**

```txt theme={null}
    -n, --name <name>
    --no-browser-restart
    --json
    -h, --help
```

<ParamField type="string">
  Browser instance name.
</ParamField>

<ParamField>
  Do not restart the browser after clearing the proxy.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Reference

* [Overview](./overview)
* [Platform reference](./platform) — Manage projects on Intuned platform.
* [Configuration](./configuration) — Settings file, environment variables, and authentication.


# Overview
Source: https://intunedhq.com/docs/main/05-references/cli/overview



The Intuned CLI `@intuned/cli` provides command-line access to develop, test, deploy and manage Intuned automation projects.

## Commands

* [Auth](#auth) — Manage authentication (login, logout, whoami).
* [Platform](./platform) — Manage projects on Intuned platform.
* [Dev](./dev) — Develop and test automations locally.
* [Configuration](./configuration) — Settings file, environment variables, and authentication.

***

## Auth

The `intuned auth` command group manages authentication.

### Login

Login to your Intuned account via browser-based OAuth. This login is reused across your machine.

```txt theme={null}
intuned auth login
```

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

<Note>
  Login has no effect when `INTUNED_API_KEY` environment variables are set.
</Note>

### Logout

Logout from your Intuned account.

```txt theme={null}
intuned auth logout
```

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

### Whoami

Display current authentication information including the authentication method, workspace, and role.

```txt theme={null}
intuned auth whoami
```

The output varies by authentication method:

* **Browser-based login** — Email, workspace ID, workspace name, and role.
* **API key** (`INTUNED_API_KEY`) — Workspace ID and workspace name.
* **Not logged in** — Instructions for how to authenticate.

Use `--json` for machine-readable output:

```txt theme={null}
intuned auth whoami --json
```

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Authentication

The CLI supports two authentication methods:

1. **Browser-based Login** (`intuned auth login`) — Browser-based OAuth login.
2. **API Key** (`INTUNED_API_KEY` environment variable) — Requires workspace ID from settings file (`workspaceId`) or `INTUNED_WORKSPACE_ID` env var.

Priority: API Key > Browser-based Login > Error

## Reference

* [Dev reference](./dev) — Develop and test automations locally.
* [Platform reference](./platform) — Manage projects on Intuned platform.
* [Configuration](./configuration) — Settings file, environment variables, and authentication.


# Platform
Source: https://intunedhq.com/docs/main/05-references/cli/platform



The Intuned CLI provides platform commands to manage projects, runs, attempts, and jobs on the Intuned platform. These commands allow you to interact with your automations running on the platform, retrieve logs and traces, and manage job executions.

## Commands

* Runs
  * [Runs List](#runs-list)
  * [Runs Get](#runs-get)
  * [Runs Start](#runs-start)
* Attempts
  * [Attempts Get](#attempts-get)
  * [Attempts Log](#attempts-log)
  * [Attempts Trace](#attempts-trace)
* Auth Sessions
  * [Auth Sessions List](#auth-sessions-list)
  * [Auth Sessions Get](#auth-sessions-get)
  * [Auth Sessions Create](#auth-sessions-create)
  * [Auth Sessions Update](#auth-sessions-update)
  * [Auth Sessions Validate](#auth-sessions-validate)
  * [Auth Sessions Delete](#auth-sessions-delete)
* Jobs
  * [Jobs List](#jobs-list)
  * [Jobs Get](#jobs-get)
  * [Jobs Create](#jobs-create)
  * [Jobs Update](#jobs-update)
  * [Jobs Trigger](#jobs-trigger)
  * [Jobs Pause](#jobs-pause)
  * [Jobs Resume](#jobs-resume)
  * [Jobs Delete](#jobs-delete)
* Job Runs
  * [Job Runs List](#job-runs-list)
  * [Job Runs Get](#job-runs-get)
  * [Job Runs Terminate](#job-runs-terminate)
* Issues
  * [Issues List](#issues-list)
  * [Issues Get](#issues-get)
* Environment Variables
  * [Env Vars List](#env-vars-list)
  * [Env Vars Get](#env-vars-get)
  * [Env Vars Create](#env-vars-create)
  * [Env Vars Update](#env-vars-update)
  * [Env Vars Delete](#env-vars-delete)
* Workspace Environment Variables
  * [Workspace Env Vars List](#workspace-env-vars-list)
  * [Workspace Env Vars Get](#workspace-env-vars-get)
  * [Workspace Env Vars Create](#workspace-env-vars-create)
  * [Workspace Env Vars Update](#workspace-env-vars-update)
  * [Workspace Env Vars Delete](#workspace-env-vars-delete)
* Project
  * [Project List](#project-list)
  * [Project Get](#project-get)
* Web Tasks
  * [Web Tasks Start](#web-tasks-start)
  * [Web Tasks Run](#web-tasks-run)
  * [Web Tasks Result](#web-tasks-result)

## Runs List

List runs for a project.

```txt theme={null}
intuned platform runs list [options]
```

**Options**

```txt theme={null}
    -p, --project-name <name>
    -l, --limit <number>
    -o, --offset <number>
    -f, --filter <filter...>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField type="int">
  Maximum number of results to return.
</ParamField>

<ParamField type="int">
  Number of results to skip (for pagination).
</ParamField>

<ParamField type="string[]">
  Filter results by field. Can be specified multiple times. Supported fields: `status`, `api_name`, `id`, `run_duration`, `started_at`, `run_type`, `job_id`, `job_run_id`.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Runs Get

Get detailed information about a run.

```txt theme={null}
intuned platform runs get <run-id> [options]
```

<ParamField type="string">
  The ID of the run to retrieve.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Runs Start

Start a new Run.

```txt theme={null}
intuned platform runs start <data> [options]
```

<ParamField type="string">
  Run configuration. Accepts a JSON string or a path to a JSON file.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Attempts Get

Get detailed information about a specific Attempt.

```txt theme={null}
intuned platform attempts get <run-id> [attempt-number] [options]
```

<ParamField type="string">
  The ID of the Run containing the Attempt.
</ParamField>

<ParamField type="int">
  The attempt number to retrieve. If omitted, an interactive prompt is shown to select from available attempts.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Attempts Log

Download automation logs for a specific Attempt.

```txt theme={null}
intuned platform attempts log <run-id> [attempt-number] [options]
```

<ParamField type="string">
  The ID of the Run containing the Attempt.
</ParamField>

<ParamField type="int">
  The attempt number to download logs for. If omitted, an interactive prompt is shown.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    -o, --output-file <path>
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField type="string">
  Output file path. Defaults to `log_<run-id>_<attempt-number>.jsonl`.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Attempts Trace

Download a Playwright trace for a specific Attempt.

```txt theme={null}
intuned platform attempts trace <run-id> [attempt-number] [options]
```

<ParamField type="string">
  The ID of the Run containing the Attempt.
</ParamField>

<ParamField type="int">
  The attempt number to download the trace for. If omitted, an interactive prompt is shown.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    -o, --output-file <path>
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField type="string">
  Output file path. Defaults to `trace_<run-id>_<attempt-number>.zip`.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Auth Sessions List

List auth sessions for a project.

```txt theme={null}
intuned platform authsessions list [options]
```

**Options**

```txt theme={null}
    -p, --project-name <name>
    -l, --limit <number>
    -o, --offset <number>
    -f, --filter <filter...>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField type="int">
  Maximum number of results to return.
</ParamField>

<ParamField type="int">
  Number of results to skip (for pagination).
</ParamField>

<ParamField type="string[]">
  Filter results by field. Can be specified multiple times.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Auth Sessions Get

Get detailed information about an auth session.

```txt theme={null}
intuned platform authsessions get <auth-session-id> [options]
```

<ParamField type="string">
  The ID of the auth session to retrieve.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Auth Sessions Create

Create a new auth session.

```txt theme={null}
intuned platform authsessions create <auth-session-id> [options]
```

<ParamField type="string">
  The ID for the new auth session.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    --input <json-or-file>
    --proxy <url>
    --wait [duration]
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField type="string">
  Credentials parameters as a JSON string or a path to a JSON file (prefixed with `@`).
</ParamField>

<ParamField type="string">
  Proxy URL to use for the auth session creation.
</ParamField>

<ParamField type="string">
  Wait for completion. Accepts an optional duration (e.g., `5m`, `30s`). Defaults to `10m` if no duration is specified.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Auth Sessions Update

Update an existing auth session.

```txt theme={null}
intuned platform authsessions update <auth-session-id> [options]
```

<ParamField type="string">
  The ID of the auth session to update.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    --input <json-or-file>
    --proxy <url>
    --wait [duration]
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField type="string">
  Credentials parameters as a JSON string or a path to a JSON file (prefixed with `@`).
</ParamField>

<ParamField type="string">
  Proxy URL to use for the auth session update.
</ParamField>

<ParamField type="string">
  Wait for completion. Accepts an optional duration (e.g., `5m`, `30s`). Defaults to `10m` if no duration is specified.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Auth Sessions Validate

Validate an existing auth session.

```txt theme={null}
intuned platform authsessions validate <auth-session-id> [options]
```

<ParamField type="string">
  The ID of the auth session to validate.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    --timeout <seconds>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField type="int">
  Timeout in seconds for the validation operation.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Auth Sessions Delete

Delete an auth session.

```txt theme={null}
intuned platform authsessions delete <auth-session-id> [options]
```

<ParamField type="string">
  The ID of the auth session to delete.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Jobs List

List Jobs for a project.

```txt theme={null}
intuned platform jobs list [options]
```

**Options**

```txt theme={null}
    -p, --project-name <name>
    -l, --limit <number>
    -o, --offset <number>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField type="int">
  Maximum number of results to return.
</ParamField>

<ParamField type="int">
  Number of results to skip (for pagination).
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Jobs Get

Get detailed information about a Job.

```txt theme={null}
intuned platform jobs get <job-id> [options]
```

<ParamField type="string">
  The ID of the Job to retrieve.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Jobs Create

Create a new Job.

```txt theme={null}
intuned platform jobs create <data> [options]
```

<ParamField type="string">
  Job configuration. Accepts a JSON string or a path to a JSON file.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Jobs Update

Update an existing Job.

```txt theme={null}
intuned platform jobs update <job-id> <data> [options]
```

<ParamField type="string">
  The ID of the Job to update.
</ParamField>

<ParamField type="string">
  Updated Job configuration. Accepts a JSON string or a path to a JSON file.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Jobs Trigger

Manually trigger a Job run.

```txt theme={null}
intuned platform jobs trigger <job-id> [options]
```

<ParamField type="string">
  The ID of the job to trigger.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Jobs Pause

Pause a Job. Scheduled runs stop until the Job is resumed.

```txt theme={null}
intuned platform jobs pause <job-id> [options]
```

<ParamField type="string">
  The ID of the Job to pause.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Jobs Resume

Resume a paused Job.

```txt theme={null}
intuned platform jobs resume <job-id> [options]
```

<ParamField type="string">
  The ID of the Job to resume.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Jobs Delete

Delete a Job. Asks for confirmation unless `-y` / `--yes` is passed.

```txt theme={null}
intuned platform jobs delete <job-id> [options]
```

<ParamField type="string">
  The ID of the Job to delete.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    -y, --yes
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField>
  Skip the confirmation prompt.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## JobRuns List

List JobRuns for a specific job.

```txt theme={null}
intuned platform jobruns list <job-id> [options]
```

<ParamField type="string">
  The ID of the Job to list runs for.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    -l, --limit <number>
    -o, --offset <number>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField type="int">
  Maximum number of results to return.
</ParamField>

<ParamField type="int">
  Number of results to skip (for pagination).
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## JobRuns Get

Get detailed information about a JobRun.

```txt theme={null}
intuned platform jobruns get <job-run-id> [options]
```

<ParamField type="string">
  The ID of the Job.
</ParamField>

<ParamField type="string">
  The ID of the JobRun to retrieve.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## JobRuns Terminate

Terminate a running JobRun. The CLI resolves the Job from the JobRun ID.

```txt theme={null}
intuned platform jobruns terminate <job-run-id> [options]
```

<ParamField type="string">
  The ID of the JobRun to terminate.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Issues List

List issues for a project.

```txt theme={null}
intuned platform issues list [options]
```

**Options**

```txt theme={null}
    -p, --project-name <name>
    -l, --limit <number>
    -o, --offset <number>
    -f, --filter <filter...>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField type="int">
  Maximum number of results to return.
</ParamField>

<ParamField type="int">
  Number of results to skip (for pagination).
</ParamField>

<ParamField type="string[]">
  Filter results by field. Can be specified multiple times.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Issues Get

Get detailed information about an issue.

```txt theme={null}
intuned platform issues get <issue-ref> [options]
```

<ParamField type="int">
  The issue reference number.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Env Vars List

List environment variables for a project.

```txt theme={null}
intuned platform env-vars list [options]
```

**Options**

```txt theme={null}
    -p, --project-name <name>
    -l, --limit <number>
    -o, --offset <number>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField type="int">
  Maximum number of results to return.
</ParamField>

<ParamField type="int">
  Number of results to skip (for pagination).
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Env Vars Get

Get details of an environment variable by key.

```txt theme={null}
intuned platform env-vars get <key> [options]
```

<ParamField type="string">
  The environment variable key.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Env Vars Create

Create a new environment variable.

```txt theme={null}
intuned platform env-vars create [options]
```

**Options**

```txt theme={null}
    -p, --project-name <name>
    --key <key>
    --value <value>
    --description <description>
    --envs <envs>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField type="string">
  Variable key. Must be 1–100 characters, alphanumeric or underscore, and cannot start with a digit.
</ParamField>

<ParamField type="string">
  Variable value. Must be 1–1000 characters.
</ParamField>

<ParamField type="string">
  Variable description. Up to 1000 characters.
</ParamField>

<ParamField type="string">
  Comma-separated environments. Supported values: `AUTHORING`, `PUBLISHED`.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Env Vars Update

Update an existing environment variable by key.

```txt theme={null}
intuned platform env-vars update <current-key> [options]
```

<ParamField type="string">
  The current key of the environment variable to update.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    --key <key>
    --value <value>
    --description <description>
    --envs <envs>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField type="string">
  Updated variable key. Must be 1–100 characters, alphanumeric or underscore, and cannot start with a digit.
</ParamField>

<ParamField type="string">
  Updated variable value. Must be 1–1000 characters.
</ParamField>

<ParamField type="string">
  Updated variable description. Up to 1000 characters.
</ParamField>

<ParamField type="string">
  Updated comma-separated environments. Supported values: `AUTHORING`, `PUBLISHED`.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Env Vars Delete

Delete an environment variable by key.

```txt theme={null}
intuned platform env-vars delete <key> [options]
```

<ParamField type="string">
  The environment variable key to delete.
</ParamField>

**Options**

```txt theme={null}
    -p, --project-name <name>
    --force
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField>
  Skip the confirmation prompt.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Workspace Env Vars List

List workspace-level environment variables.

```txt theme={null}
intuned platform workspace env-vars list [options]
```

**Options**

```txt theme={null}
    -l, --limit <number>
    -o, --offset <number>
    --json
    -h, --help
```

<ParamField type="int">
  Maximum number of results to return.
</ParamField>

<ParamField type="int">
  Number of results to skip (for pagination).
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Workspace Env Vars Get

Get details of a workspace environment variable by key.

```txt theme={null}
intuned platform workspace env-vars get <key> [options]
```

<ParamField type="string">
  The environment variable key.
</ParamField>

**Options**

```txt theme={null}
    --json
    -h, --help
```

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Workspace Env Vars Create

Create a new workspace-level environment variable.

```txt theme={null}
intuned platform workspace env-vars create [options]
```

**Options**

```txt theme={null}
    --key <key>
    --value <value>
    --description <description>
    --envs <envs>
    --json
    -h, --help
```

<ParamField type="string">
  Variable key. Must be 1–100 characters, alphanumeric or underscore, and cannot start with a digit.
</ParamField>

<ParamField type="string">
  Variable value. Must be 1–1000 characters.
</ParamField>

<ParamField type="string">
  Variable description. Up to 1000 characters.
</ParamField>

<ParamField type="string">
  Comma-separated environments. Supported values: `AUTHORING`, `PUBLISHED`.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Workspace Env Vars Update

Update an existing workspace environment variable by key.

```txt theme={null}
intuned platform workspace env-vars update <current-key> [options]
```

<ParamField type="string">
  The current key of the environment variable to update.
</ParamField>

**Options**

```txt theme={null}
    --key <key>
    --value <value>
    --description <description>
    --envs <envs>
    --json
    -h, --help
```

<ParamField type="string">
  Updated variable key. Must be 1–100 characters, alphanumeric or underscore, and cannot start with a digit.
</ParamField>

<ParamField type="string">
  Updated variable value. Must be 1–1000 characters.
</ParamField>

<ParamField type="string">
  Updated variable description. Up to 1000 characters.
</ParamField>

<ParamField type="string">
  Updated comma-separated environments. Supported values: `AUTHORING`, `PUBLISHED`.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Workspace Env Vars Delete

Delete a workspace environment variable by key.

```txt theme={null}
intuned platform workspace env-vars delete <key> [options]
```

<ParamField type="string">
  The environment variable key to delete.
</ParamField>

**Options**

```txt theme={null}
    --force
    --json
    -h, --help
```

<ParamField>
  Skip the confirmation prompt.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Project List

List all projects in the workspace.

```txt theme={null}
intuned platform project list [options]
```

**Options**

```txt theme={null}
    -l, --limit <number>
    -o, --offset <number>
    --ai-enabled
    --json
    -h, --help
```

<ParamField type="int">
  Maximum number of results to return.
</ParamField>

<ParamField type="int">
  Number of results to skip (for pagination).
</ParamField>

<ParamField>
  Only show projects with AI enabled.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Project Get

Get detailed information about the current project.

```txt theme={null}
intuned platform project get [options]
```

**Options**

```txt theme={null}
    -p, --project-name <name>
    --json
    -h, --help
```

<ParamField type="string">
  Project name. See [Project Name Resolution](#project-name-resolution).
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

## Web Tasks Start

Start a new [Web Task](/docs/main/07-web-tasks/overview). Returns the `webTaskId` immediately; use [`webtasks result`](#web-tasks-result) to fetch the result.

```txt theme={null}
intuned platform webtasks start <task> [options]
```

<ParamField type="string">
  Natural-language description of the task to run.
</ParamField>

**Options**

```txt theme={null}
    --start-url <url>
    --parameters <json-or-file>
    --output-schema <json-or-file>
    --reuse-key <key>
    --model <model>
    --json
    -h, --help
```

<ParamField type="string">
  Optional hint telling the agent where to start in the browser.
</ParamField>

<ParamField type="string">
  Free-form parameters as a JSON string or a path to a JSON file.
</ParamField>

<ParamField type="string">
  Expected output JSON Schema as a JSON string or a path to a JSON file.
</ParamField>

<ParamField type="string">
  Reuse key for persisted task resources. If omitted, the server generates one and returns it in the result.
</ParamField>

<ParamField type="string">
  Model to use: `haiku`, `sonnet`, or `opus`.
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Web Tasks Run

Start a Web Task and block until it reaches a terminal state (`completed` or `canceled`).

```txt theme={null}
intuned platform webtasks run <task> [options]
```

<ParamField type="string">
  Natural-language description of the task to run.
</ParamField>

**Options**

```txt theme={null}
    --start-url <url>
    --parameters <json-or-file>
    --output-schema <json-or-file>
    --reuse-key <key>
    --model <model>
    --wait [duration]
    --json
    -h, --help
```

<ParamField type="string">
  Optional hint telling the agent where to start in the browser.
</ParamField>

<ParamField type="string">
  Free-form parameters as a JSON string or a path to a JSON file.
</ParamField>

<ParamField type="string">
  Expected output JSON Schema as a JSON string or a path to a JSON file.
</ParamField>

<ParamField type="string">
  Reuse key for persisted task resources. If omitted, the server generates one and returns it in the result.
</ParamField>

<ParamField type="string">
  Model to use: `haiku`, `sonnet`, or `opus`.
</ParamField>

<ParamField type="string">
  Stop waiting after the given duration (for example, `30s`, `10m`, `2h`).
</ParamField>

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Web Tasks Result

Fetch the current status and result of a Web Task.

```txt theme={null}
intuned platform webtasks result <web-task-id> [options]
```

<ParamField type="string">
  The ID returned when the task was started.
</ParamField>

**Options**

```txt theme={null}
    --json
    -h, --help
```

<ParamField>
  Output as JSON.
</ParamField>

<ParamField>
  Display help for command. Will not execute the command.
</ParamField>

***

## Project Name Resolution

Commands that operate on a project resolve the project name in this order:

`--project-name` flag > `projectName` field in settings file > Error

## Reference

* [Overview](./overview)
* [Dev reference](./dev) — Develop and test automations locally.
* [Configuration](./configuration) — Settings file, environment variables, and authentication.


# Error Codes
Source: https://intunedhq.com/docs/main/05-references/error-codes



This page contains all error codes that can occur in the Intuned system. For operational reasons and cancellation codes, see [reason codes](./reason-codes).

## Error Structure

All errors in the Intuned system follow a standardized structure with the following fields:

* **`code`** - Stable, machine-readable error identifier
* **`category`** - Broad classification for retry/alert rules
* **`message`** - Human-readable description of the error
* **`retriable`** - Boolean indicating if the operation can be retried
* **`doc_url`** - Optional link to documentation for this error
* **`correlation_id`** - Optional ID for tracing the error in logs
* **`details`** - Optional additional context specific to the error

## Error Categories

### `infrastructure`

Internal system errors within the Intuned platform infrastructure. These are typically retriable and indicate temporary issues with the service.

### `execution`

Errors that occur during script or automation execution.

### `auth`

Authentication and authorization related errors. For more information about auth sessions, see the [auth sessions overview](/docs/main/02-features/auth-sessions).

### `user`

Errors caused by invalid user input or client-side issues. These are typically not retriable without correcting the input.

### `billing`

Billing and credit-related errors that prevent operations from proceeding due to account limitations. For more information about pricing and credits, see our [pricing page](/docs/main/05-references/plans-and-billing).

## Script Execution Error Codes

### `script-execution-exception`

An error occurred while running the browser automation script. this usually happen when the automation throw an un-handled exception.

### `script-no-valid-output-received`

The browser automation finished, but no valid output was received from the script. This happens if the result should be in a specific format, but the automation returned an invalid output.t

### `result-too-big-error`

The result returned from the browser automation exceeds the 2 MB size limit. This usually happens when the script returns large fields such as raw HTML, Markdown, or long text blobs. Reduce the payload by truncating long strings, extracting only the fields you need, or processing results in smaller batches. For large content that must be preserved, upload it with [`uploadFileToS3`](/docs/automation-sdks/intuned-sdk/typescript/helpers/functions/uploadFileToS3) and return the resulting reference URL instead of inlining the content in the API response.

### `script-timeout`

The browser automation request exceeded the specified timeout limit in the request/project.

### `script-unexpected-error`

Unexpected error when communicating with the script; there are multiple reasons for this error. The most popular one is capacity issues, such as insufficient memory to run the script, or CPU throttling that might cause possible issues when initializing the browser or communicating with the automation script.

### `script-process-crashed`

The runtime process has crashed, usually due to OOM or critical server issues, or new app deployment during a run execution.

## Auth Session Error Codes

For more details about working with auth sessions, see [auth sessions](/docs/main/02-features/auth-sessions).

### `auth-check-failed`

The auth session check script returned false, indicating invalid or expired credentials.

### `check-attempts-failed`

All auth session check attempts have failed for the run.

### `create-attempts-failed`

All auth session creation attempts have failed for the run.

### `post-create-check-attempts-failed`

All post-creation authentication check attempts have failed for the run.

### `auth-session-not-ready`

The authentication session is not ready for use. this happens when auth session status is expired/pending and you try to use it in a run that has auto-recreate set to false.

### `auth-session-already-in-use`

The authentication session is currently being used by another operation.

## Run Execution Error Codes

For more information about monitoring runs, see [observability, monitoring, and logs](/docs/main/02-features/observability-monitoring-logs).

### `api-attempts-failed`

All API execution attempts have failed for the run.

## Client API Error Codes

### `bad-request`

The request was malformed or contained invalid parameters.

### `unauthorized`

Authentication credentials are missing or invalid.

### `forbidden`

Access denied due to insufficient permissions.

### `not-found`

The requested resource could not be found.

### `method-not-allowed`

The HTTP method is not supported for this endpoint.

### `request-timeout`

The request took too long to process and timed out.

### `conflict`

The request conflicts with the current state of the resource.

### `unprocessable-entity`

The request was well-formed but contains semantic errors.

### `sunsetted-route`

The API route has been deprecated and is no longer available.

## Billing Related error codes

### `insufficient-credits`

Insufficient credits available to perform the requested operation.

### `insufficient-resource-credits`

Insufficient resource-specific credits available for the operation.

### `workspace-invalid-billing-state`

The workspace billing state prevents the operation from proceeding.

### `insufficient-resource-credits`

Insufficient resource credits to execute the job run.

### `max-concurrent-web-tasks-reached`

The maximum number of concurrent web tasks has been reached for the workspace. If you need higher concurrency, please reach out to [Intuned support](/docs/main/06-resources/help-and-support).

## Server Error Codes

### `not-implemented`

The requested functionality is not yet implemented.

### `internal-server-error`

General internal server error that occurred within the system infrastructure. This error code rarely happens and if it does occur, it comes with a correlation\_id and event details that you can share with [Intuned support](/docs/main/06-resources/help-and-support) to resolve any issue you face.


# Intuned.json
Source: https://intunedhq.com/docs/main/05-references/intuned-json



## Introduction

The `Intuned.json` file is the configuration file for your Intuned project. It defines various settings including workspace information, replication settings, auth sessions, deployment options, runtime defaults, and IDE metadata. This file should be placed in the root directory of your project.

When you try to modify `Intuned.json` in the IDE, we will provide you with a UI that helps you modify the underlying JSON structure and describe what each property does. You can also switch to text mode to manually modify the JSON as you see fit. On the other hand, if you are working with locally using Intuned CLI to run and deploy your project, you will need to manually edit this file and ensure the JSON structure is valid.

The main difference between the CLI configuration and the IDE configuration is that the CLI configuration requires additional properties that are used when you want to deploy your project like `workspaceId` and `projectName` but these configuration properties are not needed when working in the IDE.

## Configuration Properties

<Tabs>
  <Tab title="CLI Configuration">
    <ParamField type="string">
      Your Intuned workspace ID. If not provided here, it must be supplied via the `--workspace-id` flag during deployment.
    </ParamField>

    <ParamField type="string">
      The name of your Intuned project. If not provided here, it must be supplied via the command line when deploying using `--project-name`.
    </ParamField>

    <ParamField type="object">
      Replication settings that control how your project scales and what resources it uses.

      <Expandable>
        <ParamField type="number">
          The number of machines always deployed for this project. When requests arrive, the project scales beyond this up to `maxCount`. Must be between `0` and `maxCount`.
          Counts towards your plan's machine limit.

          **Default:** matches `maxCount` (all machines are deployed).
        </ParamField>

        <ParamField type="number">
          The maximum number of machines the project can scale up to under load. Each machine handles one API request at a time, so this is the most requests your project can process at once. This does not affect jobs.

          **Default:** `1`
        </ParamField>

        <ParamField type="string">
          The machine size to use for this project. This is applicable for both API requests and jobs.

          **Options:**

          * `"standard"`: Standard machine size (6 shared vCPUs, 2GB RAM)
          * `"large"`: Large machine size (8 shared vCPUs, 4GB RAM)
          * `"x-large"`: Extra large machine size (1 performance vCPU, 8GB RAM)

          **Default:** `"standard"`
        </ParamField>
      </Expandable>
    </ParamField>

    <ParamField type="object">
      Auth session settings for managing authentication state across browser automation runs.

      <Expandable>
        <ParamField type="boolean">
          Whether auth sessions are enabled for this project. If enabled, `auth-sessions/check.ts` API must be implemented to validate the auth session.
        </ParamField>

        <ParamField type="string">
          The type of auth session to use.

          **Options:**

          * `"API"`: Requires implementing `auth-sessions/create.ts` API to create/recreate the auth session programmatically
          * `"MANUAL"`: Uses a recorder to manually create the auth session
        </ParamField>

        <ParamField type="string">
          Recorder start URL for the recorder to navigate to when creating the auth session. Required if `type` is `"MANUAL"`. Not used if `type` is `"API"`.
        </ParamField>

        <ParamField type="string">
          Recorder finish URL for the recorder. Once this URL is reached, the recorder stops and saves the auth session. Required if `type` is `"MANUAL"`. Not used if `type` is `"API"`.
        </ParamField>

        <ParamField type="string">
          Recorder browser mode. Only applicable for `"MANUAL"` type.

          **Options:**

          * `"fullscreen"`: Launches the browser in fullscreen mode
          * `"kiosk"`: Launches the browser in kiosk mode (no address bar, no navigation controls)

          **Default:** `"fullscreen"`
        </ParamField>
      </Expandable>
    </ParamField>

    <ParamField type="object">
      Metadata used by the IDE to prefill the Run playground.

      <Expandable>
        <ParamField type="object">
          Default input used to prefill the Run playground in the IDE.

          <Expandable>
            <ParamField type="string">
              The API name to preselect in the Run playground.
            </ParamField>

            <ParamField type="object">
              Default parameters to prefill for the selected API.
            </ParamField>
          </Expandable>
        </ParamField>
      </Expandable>
    </ParamField>

    <ParamField type="boolean">
      Whether to run the deployed API in a headful browser.

      <Tip>
        Running in headful can help with some anti-bot detections. However, keep in mind that running in headful mode might increase resource usage , so keep in mind to choose appropriate machine size for you.
      </Tip>

      **Default:** `false`
    </ParamField>

    <ParamField type="object">
      Browser window size used when launching the browser.

      <Expandable>
        <ParamField type="number">
          Browser width in pixels.

          **Range:** `200` to `3840`

          **Default:** `1024`
        </ParamField>

        <ParamField type="number">
          Browser height in pixels.

          **Range:** `200` to `2160`

          **Default:** `800`
        </ParamField>
      </Expandable>
    </ParamField>

    <ParamField type="string">
      Browser engine to use for this project.

      **Options:**

      * `"chromium"`: Use Chromium
      * `"brave"`: Use Brave
    </ParamField>

    <ParamField type="object">
      Project-level third-party integration settings.

      <Expandable>
        <ParamField type="object">
          1Password integration settings.

          <Expandable>
            <ParamField type="boolean">
              Whether the 1Password integration is enabled.
            </ParamField>

            <ParamField type="string">
              The configured 1Password integration name.
            </ParamField>

            <ParamField type="string">
              The configured 1Password integration version.
            </ParamField>
          </Expandable>
        </ParamField>
      </Expandable>
    </ParamField>

    <ParamField type="string">
      The region where your Intuned project is hosted.

      **Available regions:**

      * `"us"`: United States
      * `"au"`: Australia
      * `"ca"`: Canada
      * `"nl"`: Netherlands
      * `"mx"`: Mexico
      * `"ro"`: Romania
      * `"se"`: Sweden
      * `"sg"`: Singapore
      * `"es"`: Spain
      * `"za"`: South Africa
      * `"de"`: Germany
      * `"in"`: India
      * `"hk"`: Hong Kong
      * `"jp"`: Japan
      * `"pl"`: Poland
      * `"fr"`: France
      * `"gb"`: United Kingdom

      **Default:** `"us"`
    </ParamField>

    <ParamField type="object">
      Default values that Intuned applies when the same values are not provided at run time.

      <Tip>
        Use `dev` for local CLI or IDE runs and `deployed` for runs started on Intuned Platform.
      </Tip>

      <Expandable>
        <ParamField type="object">
          Default proxy by environment. Each value can be a proxy URL string or an object with `url` and optional `regex`. Set the URL to `"intuned://auto"` to let Intuned pick a managed proxy at run time. See [Auto proxy](/docs/main/02-features/stealth-mode-captcha-solving-proxies#auto-proxy-intuned-managed) and [Proxy options](/docs/main/02-features/stealth-mode-captcha-solving-proxies#proxy-options).

          <Expandable>
            <ParamField type="string | object">
              Default proxy for local development runs. A URL string (including `"intuned://auto"`), or an object:

              <Expandable>
                <ParamField type="string">
                  Proxy URL. Accepts `"intuned://auto"`.
                </ParamField>

                <ParamField type="string">
                  RE2 pattern matched against the request URL without its query string or fragment. Matching requests use the proxy; nonmatching requests connect directly.
                </ParamField>
              </Expandable>
            </ParamField>

            <ParamField type="string | object">
              Default proxy for deployed runs. A URL string (including `"intuned://auto"`), or an object:

              <Expandable>
                <ParamField type="string">
                  Proxy URL. Accepts `"intuned://auto"`.
                </ParamField>

                <ParamField type="string">
                  RE2 pattern matched against the request URL without its query string or fragment. Matching requests use the proxy; nonmatching requests connect directly.
                </ParamField>
              </Expandable>
            </ParamField>
          </Expandable>
        </ParamField>

        <ParamField type="object">
          Default browser timezone by environment. Use a valid IANA timezone identifier.

          <Expandable>
            <ParamField type="string">
              Default browser timezone for local development runs (for example `"America/New_York"`).
            </ParamField>

            <ParamField type="string">
              Default browser timezone for deployed runs (for example `"America/New_York"`).
            </ParamField>
          </Expandable>
        </ParamField>

        <ParamField type="object">
          Default trace-saving behavior by environment.

          <Expandable>
            <ParamField type="boolean">
              Whether to save traces by default for local development runs.
            </ParamField>

            <ParamField type="boolean">
              Whether to save traces by default for deployed runs.
            </ParamField>
          </Expandable>
        </ParamField>

        <ParamField type="object">
          Default request timeout, in seconds, by environment.

          <Expandable>
            <ParamField type="number">
              Default request timeout for local development runs.

              **Default run value:** `600`
            </ParamField>

            <ParamField type="number">
              Default request timeout for deployed runs.

              **Default run value:** `600`
            </ParamField>
          </Expandable>
        </ParamField>

        <ParamField type="object">
          Default retry policy by environment.

          <Expandable>
            <ParamField type="object">
              Retry defaults for local development runs.

              <Expandable>
                <ParamField type="number">
                  Maximum number of attempts before the run is marked as failed.

                  **Default run value:** `3`
                </ParamField>
              </Expandable>
            </ParamField>

            <ParamField type="object">
              Retry defaults for deployed runs.

              <Expandable>
                <ParamField type="number">
                  Maximum number of attempts before the run is marked as failed.

                  **Default run value:** `3`
                </ParamField>
              </Expandable>
            </ParamField>
          </Expandable>
        </ParamField>

        <ParamField type="object">
          Default sink configuration by environment.

          Each environment accepts the same sink object used by run inputs. Every sink supports `type` (`"webhook"` or `"s3"`), `skipOnFail` (boolean, required), and `apisToSend` (array of API names, optional). See [Webhook Sink](/docs/client-apis/api-reference/sinks/webhook) and [AWS S3 Sink](/docs/client-apis/api-reference/sinks/s3) for the type-specific fields.

          <Expandable>
            <ParamField type="object">
              Default sink configuration for local development runs.
            </ParamField>

            <ParamField type="object">
              Default sink configuration for deployed runs.
            </ParamField>
          </Expandable>
        </ParamField>

        <ParamField type="object">
          Default auth session runtime settings by environment.

          <Expandable>
            <ParamField type="object">
              Default auth session settings for local development runs.

              <Expandable>
                <ParamField type="boolean">
                  Automatically recreate auth sessions when validation fails.
                </ParamField>

                <ParamField type="number">
                  Number of attempts for auth session creation.
                </ParamField>

                <ParamField type="number">
                  Number of attempts for auth session validation checks.
                </ParamField>

                <ParamField type="string | object">
                  Proxy for auth session operations. A URL string, or `{ url, regex? }`. Set to `null` to explicitly disable. See [Proxy options](/docs/main/02-features/stealth-mode-captcha-solving-proxies#proxy-options).
                </ParamField>

                <ParamField type="string">
                  IANA timezone identifier for auth session operations. Falls back to `defaults.timezone.dev` when omitted.
                </ParamField>
              </Expandable>
            </ParamField>

            <ParamField type="object">
              Default auth session settings for deployed runs.

              <Expandable>
                <ParamField type="boolean">
                  Automatically recreate auth sessions when validation fails.
                </ParamField>

                <ParamField type="number">
                  Number of attempts for auth session creation.
                </ParamField>

                <ParamField type="number">
                  Number of attempts for auth session validation checks.
                </ParamField>

                <ParamField type="string | object">
                  Proxy for auth session operations. A URL string, or `{ url, regex? }`. Set to `null` to explicitly disable. See [Proxy options](/docs/main/02-features/stealth-mode-captcha-solving-proxies#proxy-options).
                </ParamField>

                <ParamField type="string">
                  IANA timezone identifier for auth session operations. Falls back to `defaults.timezone.deployed` when omitted.
                </ParamField>
              </Expandable>
            </ParamField>
          </Expandable>
        </ParamField>
      </Expandable>
    </ParamField>
  </Tab>

  <Tab title="IDE Configuration">
    <ParamField type="object">
      Replication settings that control how your project scales and what resources it uses.

      <Expandable>
        <ParamField type="number">
          The number of machines always deployed for this project. When requests arrive, the project scales beyond this up to `maxCount`. Must be between `0` and `maxCount`.
          Counts towards your plan's machine limit.

          **Default:** matches `maxCount` (all machines are deployed).
        </ParamField>

        <ParamField type="number">
          The maximum number of machines the project can scale up to under load. Each machine handles one API request at a time, so this is the most requests your project can process at once. This does not affect jobs.

          **Default:** `1`
        </ParamField>

        <ParamField type="string">
          The machine size to use for this project. This is applicable for both API requests and jobs.

          **Options:**

          * `"standard"`: Standard machine size (6 shared vCPUs, 2GB RAM)
          * `"large"`: Large machine size (8 shared vCPUs, 4GB RAM)
          * `"x-large"`: Extra large machine size (1 performance vCPU, 8GB RAM)

          **Default:** `"standard"`
        </ParamField>
      </Expandable>
    </ParamField>

    <ParamField type="object">
      Auth session settings for managing authentication state across browser automation runs.

      <Expandable>
        <ParamField type="boolean">
          Whether auth sessions are enabled for this project. If enabled, `auth-sessions/check.ts` API must be implemented to validate the auth session.
        </ParamField>

        <ParamField type="string">
          The type of auth session to use.

          **Options:**

          * `"API"`: Requires implementing `auth-sessions/create.ts` API to create/recreate the auth session programmatically
          * `"MANUAL"`: Uses a recorder to manually create the auth session
        </ParamField>

        <ParamField type="string">
          Recorder start URL for the recorder to navigate to when creating the auth session. Required if `type` is `"MANUAL"`. Not used if `type` is `"API"`.
        </ParamField>

        <ParamField type="string">
          Recorder finish URL for the recorder. Once this URL is reached, the recorder stops and saves the auth session. Required if `type` is `"MANUAL"`. Not used if `type` is `"API"`.
        </ParamField>

        <ParamField type="string">
          Recorder browser mode. Only applicable for `"MANUAL"` type.

          **Options:**

          * `"fullscreen"`: Launches the browser in fullscreen mode
          * `"kiosk"`: Launches the browser in kiosk mode (no address bar, no navigation controls)

          **Default:** `"fullscreen"`
        </ParamField>
      </Expandable>
    </ParamField>

    <ParamField type="object">
      Metadata used by the IDE to prefill the Run playground.

      <Expandable>
        <ParamField type="object">
          Default input used to prefill the Run playground in the IDE.

          <Expandable>
            <ParamField type="string">
              The API name to preselect in the Run playground.
            </ParamField>

            <ParamField type="object">
              Default parameters to prefill for the selected API.
            </ParamField>
          </Expandable>
        </ParamField>
      </Expandable>
    </ParamField>

    <ParamField type="boolean">
      Whether to run the deployed API in a headful browser.

      <Tip>
        Running in headful can help with some anti-bot detections. However, keep in mind that running in headful mode might increase resource usage , so keep in mind to choose appropriate machine size for you.
      </Tip>

      **Default:** `false`
    </ParamField>

    <ParamField type="object">
      Browser window size used when launching the browser.

      <Expandable>
        <ParamField type="number">
          Browser width in pixels.

          **Range:** `200` to `3840`

          **Default:** `1024`
        </ParamField>

        <ParamField type="number">
          Browser height in pixels.

          **Range:** `200` to `2160`

          **Default:** `800`
        </ParamField>
      </Expandable>
    </ParamField>

    <ParamField type="string">
      Browser engine to use for this project.

      **Options:**

      * `"chromium"`: Use Chromium
      * `"brave"`: Use Brave
    </ParamField>

    <ParamField type="object">
      Project-level third-party integration settings.

      <Expandable>
        <ParamField type="object">
          1Password integration settings.

          <Expandable>
            <ParamField type="boolean">
              Whether the 1Password integration is enabled.
            </ParamField>

            <ParamField type="string">
              The configured 1Password integration name.
            </ParamField>

            <ParamField type="string">
              The configured 1Password integration version.
            </ParamField>
          </Expandable>
        </ParamField>
      </Expandable>
    </ParamField>

    <ParamField type="string">
      The region where your Intuned project is hosted.

      **Available regions:**

      * `"us"`: United States
      * `"au"`: Australia
      * `"ca"`: Canada
      * `"nl"`: Netherlands
      * `"mx"`: Mexico
      * `"ro"`: Romania
      * `"se"`: Sweden
      * `"sg"`: Singapore
      * `"es"`: Spain
      * `"za"`: South Africa
      * `"de"`: Germany
      * `"in"`: India
      * `"hk"`: Hong Kong
      * `"jp"`: Japan
      * `"pl"`: Poland
      * `"fr"`: France
      * `"gb"`: United Kingdom

      **Default:** `"us"`
    </ParamField>

    <ParamField type="object">
      Default values that Intuned applies when the same values are not provided at run time.

      <Tip>
        Use `dev` for local CLI or IDE runs and `deployed` for runs started on Intuned Platform.
      </Tip>

      <Expandable>
        <ParamField type="object">
          Default proxy by environment. Each value can be a proxy URL string or an object with `url` and optional `regex`. Set the URL to `"intuned://auto"` to let Intuned pick a managed proxy at run time. See [Auto proxy](/docs/main/02-features/stealth-mode-captcha-solving-proxies#auto-proxy-intuned-managed) and [Proxy options](/docs/main/02-features/stealth-mode-captcha-solving-proxies#proxy-options).

          <Expandable>
            <ParamField type="string | object">
              Default proxy for local development runs. A URL string (including `"intuned://auto"`), or an object:

              <Expandable>
                <ParamField type="string">
                  Proxy URL. Accepts `"intuned://auto"`.
                </ParamField>

                <ParamField type="string">
                  RE2 pattern matched against the request URL without its query string or fragment. Matching requests use the proxy; nonmatching requests connect directly.
                </ParamField>
              </Expandable>
            </ParamField>

            <ParamField type="string | object">
              Default proxy for deployed runs. A URL string (including `"intuned://auto"`), or an object:

              <Expandable>
                <ParamField type="string">
                  Proxy URL. Accepts `"intuned://auto"`.
                </ParamField>

                <ParamField type="string">
                  RE2 pattern matched against the request URL without its query string or fragment. Matching requests use the proxy; nonmatching requests connect directly.
                </ParamField>
              </Expandable>
            </ParamField>
          </Expandable>
        </ParamField>

        <ParamField type="object">
          Default browser timezone by environment. Use a valid IANA timezone identifier.

          <Expandable>
            <ParamField type="string">
              Default browser timezone for local development runs (for example `"America/New_York"`).
            </ParamField>

            <ParamField type="string">
              Default browser timezone for deployed runs (for example `"America/New_York"`).
            </ParamField>
          </Expandable>
        </ParamField>

        <ParamField type="object">
          Default trace-saving behavior by environment.

          <Expandable>
            <ParamField type="boolean">
              Whether to save traces by default for local development runs.
            </ParamField>

            <ParamField type="boolean">
              Whether to save traces by default for deployed runs.
            </ParamField>
          </Expandable>
        </ParamField>

        <ParamField type="object">
          Default request timeout, in seconds, by environment.

          <Expandable>
            <ParamField type="number">
              Default request timeout for local development runs.

              **Default run value:** `600`
            </ParamField>

            <ParamField type="number">
              Default request timeout for deployed runs.

              **Default run value:** `600`
            </ParamField>
          </Expandable>
        </ParamField>

        <ParamField type="object">
          Default retry policy by environment.

          <Expandable>
            <ParamField type="object">
              Retry defaults for local development runs.

              <Expandable>
                <ParamField type="number">
                  Maximum number of attempts before the run is marked as failed.

                  **Default run value:** `3`
                </ParamField>
              </Expandable>
            </ParamField>

            <ParamField type="object">
              Retry defaults for deployed runs.

              <Expandable>
                <ParamField type="number">
                  Maximum number of attempts before the run is marked as failed.

                  **Default run value:** `3`
                </ParamField>
              </Expandable>
            </ParamField>
          </Expandable>
        </ParamField>

        <ParamField type="object">
          Default sink configuration by environment.

          Each environment accepts the same sink object used by run inputs. Every sink supports `type` (`"webhook"` or `"s3"`), `skipOnFail` (boolean, required), and `apisToSend` (array of API names, optional). See [Webhook Sink](/docs/client-apis/api-reference/sinks/webhook) and [AWS S3 Sink](/docs/client-apis/api-reference/sinks/s3) for the type-specific fields.

          <Expandable>
            <ParamField type="object">
              Default sink configuration for local development runs.
            </ParamField>

            <ParamField type="object">
              Default sink configuration for deployed runs.
            </ParamField>
          </Expandable>
        </ParamField>

        <ParamField type="object">
          Default auth session runtime settings by environment.

          <Expandable>
            <ParamField type="object">
              Default auth session settings for local development runs.

              <Expandable>
                <ParamField type="boolean">
                  Automatically recreate auth sessions when validation fails.
                </ParamField>

                <ParamField type="number">
                  Number of attempts for auth session creation.
                </ParamField>

                <ParamField type="number">
                  Number of attempts for auth session validation checks.
                </ParamField>

                <ParamField type="string | object">
                  Proxy for auth session operations. A URL string, or `{ url, regex? }`. Set to `null` to explicitly disable. See [Proxy options](/docs/main/02-features/stealth-mode-captcha-solving-proxies#proxy-options).
                </ParamField>

                <ParamField type="string">
                  IANA timezone identifier for auth session operations. Falls back to `defaults.timezone.dev` when omitted.
                </ParamField>
              </Expandable>
            </ParamField>

            <ParamField type="object">
              Default auth session settings for deployed runs.

              <Expandable>
                <ParamField type="boolean">
                  Automatically recreate auth sessions when validation fails.
                </ParamField>

                <ParamField type="number">
                  Number of attempts for auth session creation.
                </ParamField>

                <ParamField type="number">
                  Number of attempts for auth session validation checks.
                </ParamField>

                <ParamField type="string | object">
                  Proxy for auth session operations. A URL string, or `{ url, regex? }`. Set to `null` to explicitly disable. See [Proxy options](/docs/main/02-features/stealth-mode-captcha-solving-proxies#proxy-options).
                </ParamField>

                <ParamField type="string">
                  IANA timezone identifier for auth session operations. Falls back to `defaults.timezone.deployed` when omitted.
                </ParamField>
              </Expandable>
            </ParamField>
          </Expandable>
        </ParamField>
      </Expandable>
    </ParamField>

    <ParamField type="object">
      Stealth mode settings that control whether requests run in stealth mode.

      <Expandable>
        <ParamField type="boolean">
          Whether to enable stealth mode for this project.

          <Warning>
            Stealth mode currently only works on Intuned Platform deployments and is not applied during local development via the CLI.
          </Warning>

          <Tip>
            Upgrade to the latest Playwright version for best stealth mode compatibility. See [Playwright and browser support](/docs/main/05-references/playwright-browsers-support) for supported versions.
          </Tip>

          **Default:** `false`
        </ParamField>
      </Expandable>
    </ParamField>

    <ParamField type="object">
      CAPTCHA solving settings that control automatic CAPTCHA detection and solving.

      <Warning>
        * CAPTCHA solving requires headful mode to be enabled.
        * it's recommended to also have stealth mode enabled and a proxy attached to the run for the best results.
        * This feature only works on the Intuned Platform and is not available during
          local CLI development.
      </Warning>

      <Tip>
        The CAPTCHA solving extension works better with newer Playwright versions. Upgrade to the latest supported version for improved solve rates. See [Playwright and browser support](/docs/main/05-references/playwright-browsers-support).
      </Tip>

      <Expandable>
        <ParamField type="boolean">
          Enable the CAPTCHA solver for this project.

          **Default:** `false`
        </ParamField>

        <ParamField type="object">
          Configuration for Cloudflare challenge solving, including Turnstile and interstitial checks.

          <Expandable>
            <ParamField type="boolean">
              Enable automatic Cloudflare CAPTCHA solving.

              **Default:** `false`
            </ParamField>
          </Expandable>
        </ParamField>

        <ParamField type="object">
          Configuration for Google reCAPTCHA solving (checkbox and invisible variants).

          <Expandable>
            <ParamField type="boolean">
              Enable automatic Google reCAPTCHA solving.

              **Default:** `false`
            </ParamField>
          </Expandable>
        </ParamField>

        <ParamField type="object">
          Configuration for AWS CAPTCHA (Amazon's bot control service) solving.

          <Expandable>
            <ParamField type="boolean">
              Enable automatic AWS CAPTCHA solving.

              **Default:** `false`
            </ParamField>
          </Expandable>
        </ParamField>

        <ParamField type="object">
          Configuration for GeeTest CAPTCHA solving.

          <Expandable>
            <ParamField type="boolean">
              Enable automatic GeeTest CAPTCHA solving.

              **Default:** `false`
            </ParamField>
          </Expandable>
        </ParamField>

        <ParamField type="object">
          Configuration for FunCaptcha (Arkose Labs) solving.

          <Expandable>
            <ParamField type="boolean">
              Enable automatic FunCaptcha solving.

              **Default:** `false`
            </ParamField>
          </Expandable>
        </ParamField>

        <ParamField type="object">
          Configuration for Lemin CAPTCHA platform solving.

          <Expandable>
            <ParamField type="boolean">
              Enable automatic Lemin CAPTCHA solving.

              **Default:** `false`
            </ParamField>
          </Expandable>
        </ParamField>

        <ParamField type="object">
          Configuration for custom image-based CAPTCHA solving using CSS selectors.

          <Expandable>
            <ParamField type="boolean">
              Enable custom CAPTCHA solving.

              **Default:** `false`
            </ParamField>

            <ParamField type="array of strings">
              CSS selectors for locating the CAPTCHA image element. Multiple selectors can be provided as fallbacks.

              **Example:** `["#captcha-image", ".custom-captcha img"]`
            </ParamField>

            <ParamField type="array of strings">
              CSS selectors for the input field where the CAPTCHA answer is entered. Multiple selectors can be provided as fallbacks.

              **Example:** `["#captcha-input", ".captcha-field input"]`
            </ParamField>

            <ParamField type="array of strings">
              CSS selectors for the button that submits the CAPTCHA answer. Multiple selectors can be provided as fallbacks.

              **Example:** `["#submit-button", "button[type='submit']"]`
            </ParamField>
          </Expandable>
        </ParamField>

        <ParamField type="object">
          Configuration for text-based CAPTCHA solving (questions or puzzles) using CSS selectors.

          <Expandable>
            <ParamField type="boolean">
              Enable text CAPTCHA solving.

              **Default:** `false`
            </ParamField>

            <ParamField type="array of strings">
              CSS selectors for the challenge text or question. Multiple selectors can be provided as fallbacks.

              **Example:** `["#captcha-question", ".challenge-text"]`
            </ParamField>

            <ParamField type="array of strings">
              CSS selectors for the input field where the answer is entered. Multiple selectors can be provided as fallbacks.

              **Example:** `["#answer-input", ".captcha-answer"]`
            </ParamField>

            <ParamField type="array of strings">
              CSS selectors for the button that submits the answer. Multiple selectors can be provided as fallbacks.

              **Example:** `["#verify-button", "button.submit"]`
            </ParamField>
          </Expandable>
        </ParamField>

        <ParamField type="object">
          Optional provider authentication for CAPTCHA solving services that require credentials.

          <Expandable>
            <ParamField type="string">
              Authentication strategy to use.

              **Options:**

              * `"bearer"`: Bearer token authentication
              * `"basic"`: Basic token authentication
              * `"apiKey"`: API key authentication
            </ParamField>

            <ParamField type="string">
              Token value used when `type` is `"bearer"` or `"basic"`.
            </ParamField>

            <ParamField type="string">
              API key value used when `type` is `"apiKey"`.
            </ParamField>
          </Expandable>
        </ParamField>

        <ParamField type="object">
          Global settings for all CAPTCHA solving operations.

          <Expandable>
            <ParamField type="boolean">
              Automatically solve detected CAPTCHAs without waiting for manual intervention.

              **Default:** `true`
            </ParamField>

            <ParamField type="number">
              Delay in milliseconds before solving the CAPTCHA after it is detected. Useful for mimicking human behavior and avoiding bot detection.

              **Default:** `2000` (2 seconds)
            </ParamField>

            <ParamField type="number">
              Maximum number of times to retry solving a CAPTCHA if solving fails.

              **Default:** `3`
            </ParamField>

            <ParamField type="number">
              Maximum time in milliseconds to wait for a CAPTCHA to be solved before the automation fails.

              **Default:** `30000` (30 seconds)
            </ParamField>
          </Expandable>
        </ParamField>
      </Expandable>
    </ParamField>
  </Tab>
</Tabs>

## Configuration Examples

<Tabs>
  <Tab title="Basic API Project">
    ```json theme={null}
    {
      "workspaceId": "your_workspace_id",
      "projectName": "my-automation-project",
      "replication": {
        "maxCount": 1,
        "size": "standard"
      },
      "stealthMode": {
        "enabled": false
      },
      "headful": false,
      "region": "us"
    }
    ```
  </Tab>

  <Tab title="Project with API Auth Sessions">
    ```json theme={null}
    {
      "workspaceId": "your_workspace_id",
      "projectName": "authenticated-scraper",
      "replication": {
        "minCount": 1,
        "maxCount": 3,
        "size": "large"
      },
      "authSessions": {
        "enabled": true,
        "type": "API"
      },
      "stealthMode": {
        "enabled": false
      },
      "headful": false,
      "region": "us"
    }
    ```
  </Tab>

  <Tab title="Project with Manual Auth Sessions">
    ```json theme={null}
    {
      "workspaceId": "your_workspace_id",
      "projectName": "manual-auth-project",
      "replication": {
        "maxCount": 1,
        "size": "standard"
      },
      "authSessions": {
        "enabled": true,
        "type": "MANUAL",
        "startUrl": "https://example.com/login",
        "finishUrl": "https://example.com/dashboard",
        "browserMode": "fullscreen"
      },
      "stealthMode": {
        "enabled": false
      },
      "headful": false,
      "region": "us"
    }
    ```
  </Tab>

  <Tab title="Jobs-Only Project">
    ```json theme={null}
    {
      "workspaceId": "your_workspace_id", 
      "projectName": "batch-processing",
      "replication": {
        "maxCount": 1,
        "size": "x-large"
      },
      "stealthMode": {
        "enabled": false
      },
      "headful": true,
      "region": "us"
    }
    ```
  </Tab>

  <Tab title="Project with Defaults and Metadata">
    ```json theme={null}
    {
      "workspaceId": "your_workspace_id",
      "projectName": "configured-project",
      "replication": {
        "maxCount": 1,
        "size": "standard"
      },
      "authSessions": {
        "enabled": true,
        "type": "API"
      },
      "browserSize": {
        "width": 1440,
        "height": 900
      },
      "defaults": {
        "proxy": {
          "dev": "http://<user>:<pass>@proxy.example.com:8080",
          "deployed": {
            "url": "http://<user>:<pass>@proxy.example.com:8080",
            "timezone": "America/New_York"
          }
        },
        "retry": {
          "dev": {
            "maximumAttempts": 2
          },
          "deployed": {
            "maximumAttempts": 4
          }
        },
        "authSession": {
          "deployed": {
            "autoRecreate": true,
            "checkAttempts": 2,
            "createAttempts": 2
          }
        }
      },
      "metadata": {
        "defaultRunPlaygroundInput": {
          "apiName": "list",
          "parameters": {}
        }
      },
      "headful": false,
      "region": "us"
    }
    ```
  </Tab>
</Tabs>


# Plans and Billing
Source: https://intunedhq.com/docs/main/05-references/plans-and-billing



## Scope

Defines plan tiers, metering units, surcharges, credits, billing cycle, billing thresholds, plan-change proration, pricing model, and product surfaces for billing and usage.

## Metering and Reporting

| Metric                        | Unit     | Granularity        |
| ----------------------------- | -------- | ------------------ |
| Compute hours                 | Time     | Minutes            |
| Machine surcharges (Large/XL) | Time     | Minutes            |
| Job Runs                      | Count    | Integer units      |
| AI credit                     | Currency | \$0.001 increments |

**Data freshness:** Usage data can take up to 15 minutes to appear. Recent activity might not be reflected immediately.

### AI usage pricing

* **Rate:** AI usage billed through Intuned is metered at the model provider's cost plus a flat 20% platform fee.
* **Rounding:** Each metered AI usage event is rounded up to the nearest \$0.001.
* **BYOK:** Requests routed through [your own API key](#bring-your-own-key-byok) are billed by your provider directly and don't incur the platform fee.

## Billing Cycle

* **Billing anchor:** 1st of every month.
* **At anchor:** Charges for extra usage from the prior period and the subscription fee for the next period.
* **Signup proration:** Plan fee is prorated when signup occurs mid-month.
* **Period scope:** Included allowances and limits apply per billing period unless stated otherwise.
* **Currency:** All prices are in USD.

## Billing thresholds

* **Purpose:** On paid plans, Intuned can charge usage mid-cycle when accrued metered usage reaches an internal billing threshold for a billed resource.
* **Scope:** Billing thresholds are separate from usage alerts and hard limits. They are internal billing controls, not customer-configured limits.
* **Invoice timing:** When a billing threshold is reached, Stripe can generate and charge a mid-cycle invoice before the monthly billing anchor.
* **Credit-forward behavior:** Any amount successfully paid on a mid-cycle threshold invoice is credited toward the next monthly invoice for that same billing period.
* **Failed payment:** If a threshold invoice cannot be collected, the account can move to **Unpaid** and service can remain paused until billing is resolved.
* **Notification:** Workspace owners receive an invoice email for successful mid-cycle threshold charges.

## Credits

* **One-time AI credit:** Eligible accounts receive a one-time \$5 AI credit at signup. Credits are granted automatically — no action required. Rolls over until consumed. Applies only to AI credit usage. If your account is not eligible, upgrade your plan or [contact support](mailto:support@intunedhq.com) to use AI features.
* **Plan AI credits (paid plans):** Monthly AI credit included per plan. Applies only to AI credit usage. **Rollover:** Does not roll over.

## Standard plans

### Comparison

| Attribute                        |           **Free Plan** |  **Developer** |    **Startup** |
| -------------------------------- | ----------------------: | -------------: | -------------: |
| Monthly price                    |                     \$0 |           \$25 |          \$120 |
| Included compute hours           |                       3 |            100 | 500 (standard) |
| Compute overage rate             |                       — |  \$0.12 / hour |  \$0.10 / hour |
| API Machines (workspace total)   |                       5 |             25 |            100 |
| Max API machines per project/app |                       1 |              5 |             15 |
| Job Runs included                |                      10 |            100 |            400 |
| Job Run overage rate             |                       — |   \$0.12 / run |   \$0.10 / run |
| Standalone runs                  |               Unlimited |      Unlimited |      Unlimited |
| Monthly AI credit                | \$5 (one-time credit)\* |            \$5 |           \$20 |
| Large machine surcharge          |           Not available | +\$0.18 / hour | +\$0.15 / hour |
| X-Large machine surcharge        |           Not available | +\$0.48 / hour | +\$0.40 / hour |
| Access to Large/XL machines      |                      No |            Yes |            Yes |
| Data retention                   |                  7 days |        30 days |        30 days |
| Advanced stealth mode            |                       — |       Included |       Included |

Notes:

* “Not available” indicates feature access is not granted on that plan.
* Surcharges are metered separately from compute hours and reported in minutes.
* \* The one-time AI credit is granted automatically to eligible accounts at signup. Eligibility is determined during account creation to prevent abuse. Accounts that are not eligible can upgrade to a paid plan or [contact support](mailto:support@intunedhq.com) to use AI features.

### Surcharges

* **Large machine:** Plan-specific hourly adder; reported in minutes.
* **X-Large machine:** Plan-specific hourly adder; reported in minutes.
* **Accrual:** Surcharges accrue only when plan grants access to the corresponding machine sizes.

### Mid-cycle plan changes

* **Downgrade:** Effective at the end of the current billing period. No immediate fee changes within the period.

* **Upgrade:** Effective immediately. Charges and credits within the current period:
  * Prorated charge for the target (higher) plan for the remainder of the period.
  * Prorated refund/credit for the unused portion of the current (lower) plan.

* **Billing anchor:** Remains on the 1st of the month.

## Enterprise plan

The Enterprise plan is for teams that need custom limits, dedicated support, or tailored configurations beyond what standard plans offer. Pricing, resource limits, and terms are set on a per-agreement basis.

[Talk to us](https://cal.com/team/intuned/get-started) to discuss your requirements.

## Product Surfaces

### Billing

* **Location:** [https://app.intuned.io/billing](https://app.intuned.io/billing)
* **Functions:** Manage plan, view invoices, manage payment method, open usage details, access support, view plans, and review credits from mid-cycle threshold invoices already paid in the current billing period.
* **Plan Limits section:** Displays API Machines Deployed and included limit, workspace type, max API machines per project/app, data retention, and access to Large/XL machine sizes.

### Usage

* **Location:** [https://app.intuned.io/usage](https://app.intuned.io/usage)
* **Timeframe:** Month selector.
* **Summary tiles:** Compute Hours, AI Credit (\$), Pages Processed, Job Runs, Large Surcharge (hours), X-Large Surcharge (hours).
* **Daily Usage chart:** Toggle metrics to compare.
* **Usage by Project table:** Project-level breakdown including Compute Hours, Large Surcharge (hrs), X-Large Surcharge (hrs), AI Credit, Job Runs, Pages Processed.
* **Data freshness:** Up to 15-minute delay.

## Constraints and Invariants

* Metered values are reported using the units and granularities specified in **Metering and Reporting**.
* Overages for compute hours and job runs apply only after included amounts are exceeded for the billing period.
* Mid-cycle threshold invoices, when paid, reduce the remaining amount billed at the monthly anchor for the same billing period.
* Access restrictions (Large/XL machines, per-project machine caps) are enforced by plan.
* Data retention limits are enforced by plan.


# Playwright and browser support
Source: https://intunedhq.com/docs/main/05-references/playwright-browsers-support



## Overview

Intuned supports a limited set of Playwright versions to ensure compatibility and optimal performance. When building automations, you must use one of the supported Playwright versions listed below.

## Supported Playwright versions

The following table shows the Playwright versions supported by Intuned across different languages:

| Playwright Version | TypeScript | Python | Stealth Mode Support |
| ------------------ | :--------: | :----: | :------------------: |
| 1.44               |      ✓     |    ✓   |           —          |
| 1.46               |      ✓     |    ✓   |           —          |
| 1.52               |      ✓     |    ✓   |           —          |
| 1.55               |      ✓     |    ✓   |           ✓          |
| 1.56               |      ✓     |    ✓   |           ✓          |
| 1.60               |      ✓     |    ✓   |           ✓          |
| 1.61               |      ✓     |    ✓   |           ✓          |

<Tip>
  For best stealth results, upgrade to the latest supported Playwright version.
  These versions include enhanced stealth capabilities that help automations
  avoid detection.
</Tip>

## Browser support

Intuned currently supports only **Chromium-based browsers** as the default browser engine. This includes the standard Chromium browser that ships with Playwright.

<Note>
  Firefox and WebKit browsers are not supported. All automations run on
  Chromium-based browsers.
</Note>

## Related docs

<CardGroup>
  <Card title="Stealth mode, CAPTCHA solving, and proxies" icon="user-secret" href="/main/02-features/stealth-mode-captcha-solving-proxies">
    Learn how to configure stealth mode to help your automations avoid detection.
  </Card>
</CardGroup>


# Reason Codes
Source: https://intunedhq.com/docs/main/05-references/reason-codes



This page contains all reason codes that explain why operations were terminated or changed state in the Intuned system.

## Run Reason Codes

### `auth-session-validate-dependency-failed`

The authentication session validation dependency failed, preventing the run from proceeding.

### `terminated`

The run was terminated due to user action.

### `job-run-terminated`

The run was terminated because the parent job run was terminated.

### `failed-to-initialize-job-run`

Failed to properly initialize the job run, preventing execution from starting.

### `cancelled-user-action`

The run was canceled by user action.

## Job Run Reason Codes

### `terminated`

The job run was terminated due to user action.

### `user-request`

The job run was paused during execution due to an explicit user request.

### `auth-session-not-found`

The required authentication session could not be found for the job run.

### `auth-session-invalid-mid-job`

The authentication session became invalid while the job was running. so the job was paused waiting for user to take action ( if needed) and make sure auth session instance is healthy

### `auth-session-validate-dependency-failed`

Before each job run, we validate the authentication session. If validation fails, we cancel the active job run and mark it as canceled with the reason of authentication session validation failure.

### `auth-session-locked`

The authentication session is locked by another run and cannot be used by this job run.

### `another-job-run-active`

Another job run is currently active, preventing this job run from starting.


# attempt_store
Source: https://intunedhq.com/docs/main/05-references/runtime-sdk-python/attempt-store



```python theme={null}
from intuned_runtime import attempt_store

attempt_store: Store
```

A key-value store scoped to the current attempt. Use it to pass initialized dependencies from hooks to your automation code, or to share data between different parts of your automation. The store is cleared when the attempt ends.

Unlike `persistent_store`, which persists across runs, `attempt_store` is temporary and only available during the current execution. It can store any Python object, not just JSON-serializable data—making it ideal for sharing initialized objects, clients, or context across your automation.

## Common use cases

**Sharing context** — Store initialized dependencies, loggers, API clients, or configuration objects that you want to access from multiple functions without passing them as arguments. Set them up in a [hook](/docs/main/01-learn/recipes/setup-hooks) and retrieve them in your automation—useful for libraries like [Stagehand](/docs/main/04-integrations/stagehand) or [Browser Use](/docs/main/04-integrations/browser-use).

**Sharing data within an automation** — Store intermediate results that you need to access later in the same execution.

## `Store`

```python theme={null}
class Store:
    def get(self, key: str, default: Any = None) -> Any: ...
    def set(self, key: str, value: Any) -> None: ...
```

## Method reference

### get

```python theme={null}
def get(self, key: str, default: Any = None) -> Any: ...
```

Retrieve a value from the attempt store.

**Parameters**

<ParamField type="str">
  The key to retrieve the value for.
</ParamField>

<ParamField type="Any">
  The default value to return if the key is not found.
</ParamField>

**Returns**

Returns the value associated with the key, or `default` if not found.

### set

```python theme={null}
def set(self, key: str, value: Any) -> None: ...
```

Store a value in the attempt store.

**Parameters**

<ParamField type="str">
  The key to set the value for.
</ParamField>

<ParamField type="Any">
  The value to store.
</ParamField>

**Returns**

Returns `None`.

## Related

* [Setup hooks recipe](/docs/main/01-learn/recipes/setup-hooks) — Learn how to initialize dependencies in hooks.
* [Stagehand integration](/docs/main/04-integrations/stagehand) — Using Stagehand with attempt\_store.
* [Browser Use integration](/docs/main/04-integrations/browser-use) — Using Browser Use with attempt\_store.


# CAPTCHA helpers
Source: https://intunedhq.com/docs/main/05-references/runtime-sdk-python/captcha-helpers



When you enable [CAPTCHA solving](/docs/main/02-features/stealth-mode-captcha-solving-proxies#captcha-solving) in your project, CAPTCHAs are solved automatically in the background. However, your automation may need to wait for a CAPTCHA to be solved before proceeding, or know when a CAPTCHA was solved.

These helpers let you wait for CAPTCHAs to be solved and react to status changes.

<Note>
  CAPTCHA is a general term for challenges that verify you're human. This includes reCAPTCHA, hCaptcha, Cloudflare Turnstile, and similar services.
</Note>

## Available helpers

* [`wait_for_captcha_solve`](#wait_for_captcha_solve) — Wait for a CAPTCHA to be solved
* [`on_captcha_event`](#on_captcha_event) — Register a callback for CAPTCHA updates
* [`once_captcha_event`](#once_captcha_event) — Register a one-time callback for a CAPTCHA update
* [`remove_captcha_event_listener`](#remove_captcha_event_listener) — Remove a previously registered callback

## Function reference

### wait\_for\_captcha\_solve

Wait for a CAPTCHA to be solved. Supports three usage patterns: callable, wrapper, and decorator.

**Note:** The decorator pattern is available in Python only. TypeScript uses direct call and wrapper patterns. In Python, the decorator can be used either as `@wait_for_captcha_solve` or `@wait_for_captcha_solve(...)`.

<Tip>
  Use the callable form only when the page is already loaded and the challenge is
  already visible. The callable form does not wait for `networkidle`. If a
  navigation, submit, or click may trigger the challenge, prefer the wrapper or
  decorator forms.
</Tip>

<Tabs>
  <Tab title="Callable">
    ```python theme={null}
    from intuned_runtime.captcha import wait_for_captcha_solve

    async def wait_for_captcha_solve(
        page: Page,
        timeout_s: float = 10.0,
        settle_period: float = 5.0,
    ) -> None: ...
    ```

    This callable form supports both `wait_for_captcha_solve(page, ...)` and
    `wait_for_captcha_solve(page=page, ...)`. The wrapper form below is keyword-only
    because it requires both `page=` and `func=`.

    **Parameters**

    <ParamField type="Page">
      Playwright page object.
    </ParamField>

    <ParamField type="float">
      Maximum wait time in seconds. Raises `TimeoutError` if exceeded.
    </ParamField>

    <ParamField type="float">
      Wait time in seconds before checking if CAPTCHAs appeared. Resets when a CAPTCHA is detected during the period.
    </ParamField>

    **Returns**

    Returns `None` when solved or settle period elapses.
  </Tab>

  <Tab title="Wrapper">
    ```python theme={null}
    from intuned_runtime.captcha import wait_for_captcha_solve

    async def wait_for_captcha_solve(
        *,
        page: Page,
        func: Callable[..., Any],
        timeout_s: float = 10.0,
        settle_period: float = 5.0,
        wait_for_network_settled: bool = True,
    ) -> Any: ...
    ```

    **Parameters**

    <ParamField type="Page">
      Playwright page object.
    </ParamField>

    <ParamField type="Callable[..., Any]">
      Function to execute before waiting for solve. Can be sync or async.
    </ParamField>

    <ParamField type="float">
      Maximum wait time in seconds. Raises `TimeoutError` if exceeded.
    </ParamField>

    <ParamField type="float">
      Wait time in seconds before checking if CAPTCHAs appeared. Resets when a CAPTCHA is detected during the period.
    </ParamField>

    <ParamField type="bool">
      Whether to wait for network idle before starting the wait for solve operation.
    </ParamField>

    **Returns**

    Returns the result of `func`, or `None` if `func` has no return value.
  </Tab>

  <Tab title="Decorator">
    ```python theme={null}
    from intuned_runtime.captcha import wait_for_captcha_solve

    def wait_for_captcha_solve(
        timeout_s: float = 10.0,
        settle_period: float = 5.0,
        wait_for_network_settled: bool = True,
    ): ...
    ```

    **Parameters**

    <ParamField type="float">
      Maximum wait time in seconds. Raises `TimeoutError` if exceeded.
    </ParamField>

    <ParamField type="float">
      Wait time in seconds before checking if CAPTCHAs appeared. Resets when a CAPTCHA is detected during the period.
    </ParamField>

    <ParamField type="bool">
      Whether to wait for network idle before starting the wait for solve operation.
    </ParamField>

    **Returns**

    Preserves the wrapped function's return type. The decorated function must accept a `Page` as its first argument.
  </Tab>
</Tabs>

<br />

**Raises**

* `TimeoutError` — Raised when `timeout_s` elapses while CAPTCHAs are still being solved.
* `CaptchaSolveError` — Raised when the CAPTCHA solver fails. Contains a [`CaptchaError`](#captchaerror) with the error code and details.
* `RuntimeError` — Raised when the subscription cannot be created or the page context is invalid.

<br />

**Examples**

<CodeGroup>
  ```python Wait after navigating to a page with CAPTCHA theme={null}
  from intuned_runtime.captcha import wait_for_captcha_solve
  from playwright.async_api import Page
  from intuned_browser import go_to_url

  async def automation(page: Page, params, **_kwargs):
      await go_to_url(page=page, url="https://www.scrapingcourse.com/login/cf-turnstile")
      await wait_for_captcha_solve(page=page, timeout_s=120.0, settle_period=5.0)
  ```

  ```python Navigate or click, then wait for CAPTCHA solve theme={null}
  from intuned_runtime.captcha import wait_for_captcha_solve
  from playwright.async_api import Page
  from intuned_browser import go_to_url

  async def automation(page: Page, params, **_kwargs):
      await wait_for_captcha_solve(
          page=page,
          func=lambda: go_to_url(page=page, url="https://www.scrapingcourse.com/login/cf-turnstile"),
          timeout_s=120.0,
          settle_period=5.0,
          wait_for_network_settled=True,
      )
  ```

  ```python Reusable decorator for form submission theme={null}
  from intuned_runtime.captcha import wait_for_captcha_solve
  from playwright.async_api import Page

  @wait_for_captcha_solve
  async def submit_and_wait(page: Page):
      await page.click('#submit')

  async def automation(page: Page, params, **_kwargs):
      await submit_and_wait(page)
  ```
</CodeGroup>

### on\_captcha\_event

```python theme={null}
from intuned_runtime.captcha import on_captcha_event, Captcha, CaptchaStatus
from playwright.async_api import Page

async def on_captcha_event(
    page: Page,
    status: CaptchaStatus,
    f: Callable[[Captcha], Awaitable[None] | None],
) -> None: ...
```

Register a callback for CAPTCHA updates.

Subscribe to CAPTCHA updates on a page. The callback fires every time a CAPTCHA with the specified status is observed. The subscription remains active until the page or context is destroyed. Use this for monitoring, logging, or reacting to CAPTCHA status.

<br />

**Parameters**

<ParamField type="Page">
  Playwright page object.
</ParamField>

<ParamField type={<a href="#captchastatus">CaptchaStatus</a>}>
  The CAPTCHA status to listen for.
</ParamField>

<ParamField type={<span>Callable[[<a href="#captcha">Captcha</a>], Awaitable[None] | None]</span>}>
  Callback function that executes when the status is observed. Receives a `Captcha` instance as its only argument.
</ParamField>

<br />

**Returns**

Returns `None`. The function subscribes and returns immediately. The callback fires each time a CAPTCHA with the specified status is observed.

<br />

**Raises**

* `RuntimeError` — Raised when the subscription cannot be created or the page context is invalid.

**Example**

<CodeGroup>
  ```python Log CAPTCHA status changes theme={null}
  from intuned_runtime.captcha import on_captcha_event, Captcha
  from playwright.async_api import Page


  async def handle_captcha_event(captcha: Captcha) -> None:
      print(f"Captcha {captcha.id} (tab={captcha.tab_id}) status={captcha.status} retries={captcha.retry_count}")

      if captcha.status == "solved":
          print(f"Solved after {captcha.retry_count} retries")
      elif captcha.status == "error":
          print(f"Solve error: {captcha.error}")


  await on_captcha_event(page, status='solved', f=handle_captcha_event)
  ```
</CodeGroup>

### once\_captcha\_event

```python theme={null}
from intuned_runtime.captcha import once_captcha_event, Captcha, CaptchaStatus
from playwright.async_api import Page

async def once_captcha_event(
    page: Page,
    status: CaptchaStatus,
    f: Callable[[Captcha], Awaitable[None] | None],
) -> None: ...
```

Register a one-time callback for a CAPTCHA update.

Subscribe to CAPTCHA updates on a page. The callback fires once when a CAPTCHA with the specified status is observed, then automatically unsubscribes. Use this when you need to respond only to the next occurrence, such as recording when a CAPTCHA is solved or performing cleanup.

<br />

**Parameters**

<ParamField type="Page">
  Playwright page object.
</ParamField>

<ParamField type={<a href="#captchastatus">CaptchaStatus</a>}>
  The CAPTCHA status to listen for.
</ParamField>

<ParamField type={<span>Callable[[<a href="#captcha">Captcha</a>], Awaitable[None] | None]</span>}>
  Callback function that executes when the status is observed. Receives a `Captcha` instance as its only argument.
</ParamField>

<br />

**Returns**

Returns `None`. The function subscribes and returns immediately. The callback fires at most once.

<br />

**Raises**

* `RuntimeError` — Raised when the subscription cannot be created or the page context is invalid.

**Example**

<CodeGroup>
  ```python One-time notification on solve theme={null}
  from intuned_runtime.captcha import once_captcha_event, Captcha
  from playwright.async_api import Page


  async def handle_solve_once(captcha: Captcha) -> None:
      print(f"One-time notify: captcha {captcha.id} status={captcha.status} retries={captcha.retry_count}")


  await once_captcha_event(page, status='solved', f=handle_solve_once)
  ```
</CodeGroup>

### remove\_captcha\_event\_listener

```python theme={null}
from intuned_runtime.captcha import remove_captcha_event_listener, Captcha, CaptchaStatus
from playwright.async_api import Page
from typing import Callable, Awaitable

async def remove_captcha_event_listener(
    page: Page,
    status: CaptchaStatus,
    f: Callable[[Captcha], Awaitable[None] | None],
) -> None: ...
```

Remove a previously registered CAPTCHA callback.

Unsubscribe a callback registered using `on_captcha_event` or `once_captcha_event`. You must pass the same page, status, and callback function that were used to register the callback.

<br />

**Parameters**

<ParamField type="Page">
  Playwright page object.
</ParamField>

<ParamField type="CaptchaStatus">
  The CAPTCHA status that the listener was registered for.
</ParamField>

<ParamField type="Callable[[Captcha], Awaitable[None] | None]">
  The callback function that was originally registered. Must be the same function reference.
</ParamField>

<br />

**Returns**

Returns `None`. The function unsubscribes the callback and returns immediately.

<br />

**Raises**

* `RuntimeError` — Raised when the callback cannot be removed or the page context is invalid.

**Example**

<CodeGroup>
  ```python Subscribe and unsubscribe theme={null}
  from intuned_runtime.captcha import on_captcha_event, remove_captcha_event_listener, Captcha
  from playwright.async_api import Page


  async def handle_captcha_event(captcha: Captcha) -> None:
      print(f"Captcha status: {captcha.status}")


  # Subscribe to CAPTCHA updates
  await on_captcha_event(page, status='solved', f=handle_captcha_event)

  # Later, unsubscribe
  await remove_captcha_event_listener(page, status='solved', f=handle_captcha_event)
  ```
</CodeGroup>

**Note:** The callback function reference must match exactly. Anonymous functions cannot be removed. Callbacks registered with `once_captcha_event` are automatically removed after firing.

## Best practices

* Use the wrapper or decorator forms when a navigation, submit, or click may trigger the challenge.
* Use the callable form only after the page has already settled and the CAPTCHA is already present.
* Set timeout values high enough for real challenges. `60.0` to `120.0` seconds is a safer default than `10.0` seconds for production flows.
* Adjust the settle period for the wait before checking status. It resets when CAPTCHAs are detected.
* Leave `wait_for_network_settled` enabled for wrapper and decorator usage unless you have a reason to skip it.
* Subscribe to CAPTCHA updates using `on_captcha_event` or `once_captcha_event` for telemetry and monitoring.
* Store callback function references if you need to unsubscribe later.

## Type reference

### `Captcha`

```python theme={null}
from pydantic import BaseModel, Field
from intuned_runtime.captcha import Captcha, CaptchaStatus, CaptchaError

class Captcha(BaseModel):
    model_config = {
        "populate_by_name": True,
        "serialize_by_alias": True,
    }
    id: str
    tab_id: int = Field(alias="tabId", default=0)
    type: str
    status: CaptchaStatus
    retry_count: int = Field(alias="retryCount", default=0)
    error: CaptchaError | None = None
```

**Properties**

<ParamField type="str">
  Unique identifier for the CAPTCHA observation.
</ParamField>

<ParamField type="int">
  Browser tab ID where the CAPTCHA was detected.
</ParamField>

<ParamField type="str">
  CAPTCHA provider type, such as `recaptcha`, `hcaptcha`, or `cloudflare`.
</ParamField>

<ParamField type={<a href="#captchastatus">CaptchaStatus</a>}>
  Current solving state.
</ParamField>

<ParamField type="int">
  Number of solve attempts made. Defaults to `0`.
</ParamField>

<ParamField type={<span><a href="#captchaerror">CaptchaError</a> | None</span>}>
  Error details when `status` is `error`, otherwise `None`.
</ParamField>

### `CaptchaStatus`

```python theme={null}
from typing import Literal
from intuned_runtime.captcha import CaptchaStatus

CaptchaStatus = Literal["attached", "solving", "solved", "error", "detached"]
```

**Values**

* **`attached`** — CAPTCHA element detected in the page. Use this to know when a challenge appears.
* **`solving`** — Solver is actively processing the CAPTCHA.
* **`solved`** — CAPTCHA solved successfully. Resume your workflow.
* **`error`** — Solver failed. Check the `error` field for details.
* **`detached`** — CAPTCHA element removed from the page. Treat as cancelled.

### `CaptchaError`

```python theme={null}
from pydantic import BaseModel
from typing import Any
from intuned_runtime.captcha import CaptchaError, CaptchaErrorCode

class CaptchaError(BaseModel):
    code: CaptchaErrorCode
    error: Any | None = None
```

**Properties**

<ParamField type={<a href="#captchaerrorcode">CaptchaErrorCode</a>}>
  Error code indicating the type of failure.
</ParamField>

<ParamField type="Any | None">
  Additional error details, if available.
</ParamField>

### `CaptchaErrorCode`

```python theme={null}
from typing import Literal
from intuned_runtime.captcha import CaptchaErrorCode

CaptchaErrorCode = Literal[
    "HIT_LIMIT",
    "MAX_RETRIES",
    "UNEXPECTED_ERROR",
    "UNEXPECTED_SERVER_RESPONSE"
]
```

**Values**

* **`HIT_LIMIT`** — Reached billing limits for CAPTCHA solves. See [Plans and billing](/docs/main/05-references/plans-and-billing) for details on limits and upgrading.
* **`MAX_RETRIES`** — Exceeded maximum retry attempts specified in [`settings.maxRetries`](/docs/main/02-features/stealth-mode-captcha-solving-proxies#settings-reference).
* **`UNEXPECTED_ERROR`** — An unexpected error occurred while solving. This is a solver error and not related to your automation.
* **`UNEXPECTED_SERVER_RESPONSE`** — The solver received an unexpected response. This is a solver error and not related to your automation.


# extend_payload
Source: https://intunedhq.com/docs/main/05-references/runtime-sdk-python/extend-payload



```python theme={null}
from intuned_runtime import extend_payload

def extend_payload(*payload: Payload) -> None: ...
```

Adds additional payloads to the current job execution. Use this for dynamic scheduling—when your automation discovers new work that should be processed as part of the same job run.

For example, when scraping a paginated list, you can extend the job with payloads for each subsequent page as you discover them.

Implicitly calls [`extend_timeout`](./extend-timeout) to ensure the job has time to process the new payloads.

<Note>
  This function only works in the context of a Job. It has no effect when running automations directly via the API.
</Note>

**Parameters**

<ParamField type={<a href="#payload">Payload</a>}>
  One or more payloads to add to the job execution.
</ParamField>

**Returns**

Returns `None`.

## `Payload`

```python theme={null}
from typing import Any, TypedDict

class Payload(TypedDict):
    api: str
    parameters: dict[str, Any]
```

**Properties**

<ParamField type="str">
  The name of the API to execute.
</ParamField>

<ParamField type="dict[str, Any]">
  The parameters to pass to the API.
</ParamField>

## Related

* [Extend payload recipe](/docs/main/01-learn/recipes/extend-payload) — Examples and patterns for dynamic scheduling.
* [extend\_timeout](/docs/main/05-references/runtime-sdk-python/extend-timeout) — Reset the timeout timer for the current attempt.
* [Jobs](/docs/main/02-features/jobs-batched-executions) — Learn about job execution and scheduling.


# extend_timeout
Source: https://intunedhq.com/docs/main/05-references/runtime-sdk-python/extend-timeout



```python theme={null}
from intuned_runtime import extend_timeout

def extend_timeout() -> None: ...
```

Resets the timeout timer for the current attempt. Use this when your automation needs more time than the configured timeout allows—for example, when processing a large dataset or waiting for slow network responses.

When called, the attempt gets a fresh timeout period equal to the original timeout duration. This function doesn't block execution.

<Note>
  This function only works in the context of a Job. It has no effect when running automations directly via the API.
</Note>

**Returns**

Returns `None`.

## Related

* [extend\_payload](/docs/main/05-references/runtime-sdk-python/extend-payload) — Add additional payloads to the current job execution.


# get_auth_session_parameters
Source: https://intunedhq.com/docs/main/05-references/runtime-sdk-python/get-auth-session-parameters



```python theme={null}
from intuned_runtime import get_auth_session_parameters

async def get_auth_session_parameters() -> Any: ...
```

Returns the parameters used to create the current AuthSession. Use this to access credentials or configuration values that were provided when the AuthSession was created.

This function only works with Credentials-based AuthSessions. It allows your automation to access the original parameters (like usernames, API keys, or other identifiers) without hardcoding them.

**Returns**

Returns `Any` — The parameters object used to create the AuthSession.

**Raises**

* `Error` — Raised if the project doesn't use AuthSessions.
* `Error` — Raised if the AuthSession is Recorder-based (not Credentials-based).
* `Error` — Raised if the AuthSession is Runtime-based.

## Example: branching on a login flow

When a single site exposes more than one login flow (for example, separate provider and patient logins on the same portal), include a discriminator parameter when the AuthSession is created and read it back inside `check` to validate the correct post-login state.

```python auth-sessions/create.py theme={null}
from playwright.async_api import Page
from typing import Literal, TypedDict


class Params(TypedDict):
    loginType: Literal["provider", "patient"]
    username: str
    password: str


async def create(page: Page, params: Params | None = None, **_kwargs):
    await page.goto("https://portal.example.com/login")
    await page.get_by_placeholder("Username").fill(params["username"])
    await page.get_by_placeholder("Password").fill(params["password"])
    await page.get_by_role("button", name="Sign in").click()

    dashboard_path = (
        "/provider/dashboard"
        if params["loginType"] == "provider"
        else "/patient/home"
    )
    await page.wait_for_url(f"https://portal.example.com{dashboard_path}")
```

```python auth-sessions/check.py theme={null}
from playwright.async_api import Page
from intuned_runtime import get_auth_session_parameters


async def check(page: Page, **_kwargs) -> bool:
    params = await get_auth_session_parameters()

    path = (
        "/provider/dashboard"
        if params["loginType"] == "provider"
        else "/patient/home"
    )
    await page.goto(f"https://portal.example.com{path}")

    return "/login" not in page.url
```

When the AuthSession is created with `parameters.loginType: "provider"`, both `create` and `check` follow the provider flow; passing `"patient"` switches both to the patient flow.

## Related

* [AuthSessions](/docs/main/02-features/auth-sessions) — Learn about authentication session management.
* [Multiple login flows on the same site](/docs/main/02-features/auth-sessions#usage-patterns) — Usage pattern this example supports.


# Non-retryable errors
Source: https://intunedhq.com/docs/main/05-references/runtime-sdk-python/non-retryable-error



Use `NonRetryableError` when retrying an automation cannot succeed without changing its input or configuration.

```python theme={null}
from intuned_runtime import NonRetryableError


async def automation(params: dict):
    if not params.get("accountId"):
        raise NonRetryableError("accountId is required")

    # Continue the automation.
```

The error accepts a single message argument. When it is raised:

* The current attempt fails.
* Intuned skips any remaining configured attempts for that automation phase.
* During AuthSession:Validate runs with auto-recreate enabled, if the error occurs during the first checks, the auth session will be recreated.


# Overview
Source: https://intunedhq.com/docs/main/05-references/runtime-sdk-python/overview



The Intuned Runtime SDK for Python (`intuned-runtime`) is a required dependency for all Intuned projects. It provides:

1. Runtime setup and configuration for Playwright that integrates with the Intuned platform.
2. The Intuned CLI for developing, testing, and deploying automations locally.
3. Helper functions that give your automations access to Intuned platform features.

All Intuned projects require the Runtime SDK as a dependency. When you deploy a project, the latest version of the Runtime SDK is used.

## Reference

* [persistent\_store](./persistent-store) — Key-value store that persists across runs.
* [attempt\_store](./attempt-store) — Key-value store scoped to the current attempt.
* [extend\_payload](./extend-payload) — Add payloads dynamically during job execution.
* [extend\_timeout](./extend-timeout) — Reset the timeout timer for the current attempt.
* [get\_auth\_session\_parameters](./get-auth-session-parameters) — Retrieve parameters of the current AuthSession.
* [CAPTCHA helpers](./captcha-helpers) — Wait for CAPTCHAs to be solved and subscribe to status updates.
* [Non-retryable errors](./non-retryable-error) — Stop a failed run without executing its remaining retries.
* [CLI reference](../cli) — Command-line interface for developing, testing, and deploying automations.


# persistent_store
Source: https://intunedhq.com/docs/main/05-references/runtime-sdk-python/persistent-store



```python theme={null}
from intuned_runtime import persistent_store

persistent_store: PersistentStore
```

A persistent key-value store that maintains data across different API executions. This allows you to share data between different runs of your Projects.

**Constraints:**

* Keys must be at least 1 character long.
* Keys cannot contain `:` (colon) or `#` (hash) characters.
* Data stored must be JSON-serializable.

## `PersistentStore`

```python theme={null}
class PersistentStore:
    async def get(self, key: str) -> Any: ...
    async def set(self, key: str, value: Any) -> None: ...
```

## Method reference

### get

```python theme={null}
async def get(self, key: str) -> Any: ...
```

Retrieve a value from the persistent store.

**Parameters**

<ParamField type="str">
  The key to retrieve the value for. Must be at least 1 character long and cannot contain `:` or `#` characters.
</ParamField>

**Returns**

Returns the value associated with the key, or `None` if not found.

**Raises**

* `ValueError` — Raised if the key is invalid (empty or contains forbidden characters).

### set

```python theme={null}
async def set(self, key: str, value: Any) -> None: ...
```

Store a value in the persistent store.

**Parameters**

<ParamField type="str">
  The key to set the value for. Must be at least 1 character long and cannot contain `:` or `#` characters.
</ParamField>

<ParamField type="Any">
  The value to store. Can be any JSON-serializable type (dict, list, str, int, float, bool, None).
</ParamField>

**Returns**

Returns `None` when the value is successfully stored.

**Raises**

* `ValueError` — Raised if the key is invalid (empty or contains forbidden characters).

## Related

* [KV cache recipe](/docs/main/01-learn/recipes/kv-cache) — Examples and patterns for using key-value stores.
* [attempt\_store](/docs/main/05-references/runtime-sdk-python/attempt-store) — Store data scoped to the current attempt.


# attemptStore
Source: https://intunedhq.com/docs/main/05-references/runtime-sdk-typescript/attempt-store



```typescript theme={null}
import { attemptStore } from '@intuned/runtime';

attemptStore: Store
```

A key-value store scoped to the current attempt. Use it to pass initialized dependencies from hooks to your automation code, or to share data between different parts of your automation. The store is cleared when the attempt ends.

Unlike `persistentStore`, which persists across runs, `attemptStore` is temporary and only available during the current execution. It can store any JavaScript object, not just JSON-serializable data—making it ideal for sharing initialized objects, clients, or context across your automation.

## Common use cases

**Sharing context** — Store initialized dependencies, loggers, API clients, or configuration objects that you want to access from multiple functions without passing them as arguments. Set them up in a [hook](/docs/main/01-learn/recipes/setup-hooks) and retrieve them in your automation—useful for libraries like [Stagehand](/docs/main/04-integrations/stagehand) or [Browser Use](/docs/main/04-integrations/browser-use).

**Sharing data within an automation** — Store intermediate results that you need to access later in the same execution.

## `Store`

```typescript theme={null}
interface Store {
  get(key: string): any;
  set(key: string, value: any): void;
}
```

## Method reference

### get

```typescript theme={null}
get(key: string): any
```

Retrieve a value from the attempt store.

**Parameters**

<ParamField type="string">
  The key to retrieve the value for.
</ParamField>

**Returns**

Returns the value associated with the key, or `undefined` if not found.

### set

```typescript theme={null}
set(key: string, value: any): void
```

Store a value in the attempt store.

**Parameters**

<ParamField type="string">
  The key to set the value for.
</ParamField>

<ParamField type="any">
  The value to store.
</ParamField>

**Returns**

Returns `void`.

## Related

* [Setup hooks recipe](/docs/main/01-learn/recipes/setup-hooks) — Learn how to initialize dependencies in hooks.
* [Stagehand integration](/docs/main/04-integrations/stagehand) — Using Stagehand with attemptStore.
* [Browser Use integration](/docs/main/04-integrations/browser-use) — Using Browser Use with attemptStore.


# CAPTCHA helpers
Source: https://intunedhq.com/docs/main/05-references/runtime-sdk-typescript/captcha-helpers



When you enable [CAPTCHA solving](/docs/main/02-features/stealth-mode-captcha-solving-proxies#captcha-solving) in your project, CAPTCHAs are solved automatically in the background. However, your automation may need to wait for a CAPTCHA to be solved before proceeding, or know when a CAPTCHA was solved.

These helpers let you wait for CAPTCHAs to be solved and react to status changes.

<Note>
  CAPTCHA is a general term for challenges that verify you're human. This includes reCAPTCHA, hCaptcha, Cloudflare Turnstile, and similar services.
</Note>

## Available helpers

* [`waitForCaptchaSolve`](#waitforcaptchasolve) — Wait for a CAPTCHA to be solved
* [`withWaitForCaptchaSolve`](#withwaitforcaptchasolve) — Execute a callback then wait for CAPTCHA solve
* [`onCaptchaEvent`](#oncaptchaevent) — Register a callback for CAPTCHA updates
* [`onceCaptchaEvent`](#oncecaptchaevent) — Register a one-time callback for a CAPTCHA update
* [`removeCaptchaEventListener`](#removecaptchaeventlistener) — Remove a previously registered callback

## Function reference

### waitForCaptchaSolve

```typescript theme={null}
import { waitForCaptchaSolve } from '@intuned/runtime';
import type { Page } from 'playwright';

export declare function waitForCaptchaSolve(
  page: Page,
  options?: {
    timeoutInMs?: number;
    settleDurationMs?: number;
  }
): Promise<void>;
```

Wait for a CAPTCHA to be solved.

Listens for CAPTCHA updates and resolves when all CAPTCHAs are solved or operation times out. Used when a CAPTCHA appears and you need to block until it's solved.

<Tip>
  Use `waitForCaptchaSolve` when the page is already loaded and the challenge is
  already visible. This direct form does not wait for `networkidle` before it
  starts checking solve status. If a navigation, submit, or click may trigger the
  challenge, prefer [`withWaitForCaptchaSolve`](#withwaitforcaptchasolve).
</Tip>

<br />

**Parameters**

<ParamField type="Page">
  Playwright page object.
</ParamField>

<ParamField type="object">
  Configuration options for the wait behavior.
</ParamField>

<ParamField type="number">
  Maximum wait time in milliseconds. Throws `TimeoutError` if exceeded.
</ParamField>

<ParamField type="number">
  Wait time in milliseconds before checking if CAPTCHAs appeared. Resets when a CAPTCHA is detected during the period.
</ParamField>

<br />

**Returns**

Returns `Promise<void>` when solved or settle period elapses.

<br />

**Raises**

* `TimeoutError` — Thrown when `timeoutInMs` elapses while CAPTCHAs are still being solved.
* `CaptchaSolveError` — Thrown when the CAPTCHA solver fails. Contains a [`CaptchaError`](#captchaerror) with the error code and details.
* `Error` — Thrown when listeners cannot be attached or the page context is invalid.

### withWaitForCaptchaSolve

```typescript theme={null}
import { withWaitForCaptchaSolve } from '@intuned/runtime';
import type { Page } from 'playwright';

export declare function withWaitForCaptchaSolve<T>(
  callback: (page: Page) => Promise<T>,
  options: {
    page: Page;
    timeoutInMs?: number;
    settleDurationMs?: number;
    waitForNetworkSettled?: boolean;
  }
): Promise<T>;
```

Execute a callback then wait for CAPTCHA to be solved.

Execute the provided callback function, then listen for CAPTCHA updates and resolve when all CAPTCHAs are solved or operation times out. Useful when you need to execute a method that triggers a CAPTCHA and must block until it's solved.

<br />

**Parameters**

<ParamField type="(page: Page) => Promise<T>">
  Function to execute before waiting for solve. Receives the page object and can return a value.
</ParamField>

<ParamField type="object">
  Configuration options for the wait behavior.
</ParamField>

<ParamField type="Page">
  Playwright page object.
</ParamField>

<ParamField type="number">
  Maximum wait time in milliseconds. Throws `TimeoutError` if exceeded.
</ParamField>

<ParamField type="number">
  Wait time in milliseconds before checking if CAPTCHAs appeared. Resets when a CAPTCHA is detected during the period.
</ParamField>

<ParamField type="boolean">
  Whether to wait for network idle before checking solve status.
</ParamField>

<br />

**Returns**

Returns `Promise<T>` with the result of the callback function.

<br />

**Raises**

* `TimeoutError` — Thrown when `timeoutInMs` elapses while CAPTCHAs are still being solved.
* `CaptchaSolveError` — Thrown when the CAPTCHA solver fails. Contains a [`CaptchaError`](#captchaerror) with the error code and details.
* `Error` — Thrown if something unexpected happens.

**Examples**

<CodeGroup>
  ```typescript Wait after navigating to a page with CAPTCHA theme={null}
  import { waitForCaptchaSolve } from '@intuned/runtime';
  import { goToUrl } from '@intuned/browser';
  import type { Page } from 'playwright';

  export async function automation(page: Page, params: any) {
    await goToUrl({ page, url: "https://www.google.com/recaptcha/api2/demo" });
    await waitForCaptchaSolve(page, {
      timeoutInMs: 120000,
      settleDurationMs: 5000
    });
  }
  ```

  ```typescript Navigate or click, then wait for CAPTCHA solve theme={null}
  import { withWaitForCaptchaSolve } from '@intuned/runtime';
  import { goToUrl } from '@intuned/browser';
  import type { Page } from 'playwright';

  export async function automation(page: Page, params: any) {
    const data = await withWaitForCaptchaSolve(
      async (page) => {
        await goToUrl({ page, url: "https://www.google.com/recaptcha/api2/demo" });
        return await page.textContent('.success-message');
      },
      {
        page,
        timeoutInMs: 120000,
        settleDurationMs: 5000,
        waitForNetworkSettled: true
      }
    );

    console.log('Form submitted:', data);
  }
  ```
</CodeGroup>

### onCaptchaEvent

```typescript theme={null}
import { onCaptchaEvent } from '@intuned/runtime';
import type { Page } from 'playwright';
import type { Captcha, CaptchaStatus } from '@intuned/runtime';

export declare function onCaptchaEvent(
  page: Page,
  status: CaptchaStatus,
  f: (captcha: Captcha) => Promise<void> | void
): Promise<void>;
```

Register a callback for CAPTCHA updates.

Subscribe to CAPTCHA updates on a page. The callback fires every time a CAPTCHA with the specified status is observed. The subscription remains active until the page or context is destroyed. Use this for monitoring, logging, or reacting to CAPTCHA status.

<br />

**Parameters**

<ParamField type="Page">
  Playwright page object.
</ParamField>

<ParamField type={<a href="#captchastatus">CaptchaStatus</a>}>
  The CAPTCHA status to listen for.
</ParamField>

<ParamField type={<span>(captcha: <a href="#captcha">Captcha</a>) =&gt; Promise&lt;void&gt; | void</span>}>
  Callback function that executes when the status is observed. Receives a `Captcha` object as its only argument.
</ParamField>

<br />

**Returns**

Returns `Promise<void>`. The function subscribes and resolves immediately. The callback fires each time a CAPTCHA with the specified status is observed.

<br />

**Raises**

* `RuntimeError` — Thrown when listeners cannot be attached or the page context is invalid.

**Example**

<CodeGroup>
  ```typescript Log CAPTCHA status changes theme={null}
  import { onCaptchaEvent, type Captcha } from '@intuned/runtime';
  import type { Page } from 'playwright';

  async function handleCaptchaEvent(captcha: Captcha): Promise<void> {
    console.log(`Captcha ${captcha.id} (tab=${captcha.tabId}) status=${captcha.status} retries=${captcha.retryCount}`);

    if (captcha.status === "solved") {
      console.log(`Solved after ${captcha.retryCount} retries`);
    } else if (captcha.status === "error") {
      console.log(`Solve error: ${captcha.error}`);
    }
  }

  await onCaptchaEvent(page, 'solved', handleCaptchaEvent);
  ```
</CodeGroup>

### onceCaptchaEvent

```typescript theme={null}
import { onceCaptchaEvent } from '@intuned/runtime';
import type { Page } from 'playwright';
import type { Captcha, CaptchaStatus } from '@intuned/runtime';

export declare function onceCaptchaEvent(
  page: Page,
  status: CaptchaStatus,
  f: (captcha: Captcha) => Promise<void> | void
): Promise<void>;
```

Register a one-time callback for a CAPTCHA update.

Subscribe to CAPTCHA updates on a page. The callback fires once when a CAPTCHA with the specified status is observed, then automatically unsubscribes. Use this when you need to respond only to the next occurrence, such as recording when a CAPTCHA is solved or performing cleanup.

<br />

**Parameters**

<ParamField type="Page">
  Playwright page object.
</ParamField>

<ParamField type={<a href="#captchastatus">CaptchaStatus</a>}>
  The CAPTCHA status to listen for.
</ParamField>

<ParamField type={<span>(captcha: <a href="#captcha">Captcha</a>) =&gt; Promise&lt;void&gt; | void</span>}>
  Callback function that executes when the status is observed. Receives a `Captcha` object as its only argument.
</ParamField>

<br />

**Returns**

Returns `Promise<void>`. The function subscribes and resolves immediately. The callback fires at most once.

<br />

**Raises**

* `RuntimeError` — Thrown when listeners cannot be attached or the page context is invalid.

**Example**

<CodeGroup>
  ```typescript One-time notification on solve theme={null}
  import { onceCaptchaEvent, type Captcha } from '@intuned/runtime';
  import type { Page } from 'playwright';

  async function handleSolveOnce(captcha: Captcha): Promise<void> {
    console.log(`One-time notify: captcha ${captcha.id} status=${captcha.status} retries=${captcha.retryCount}`);
  }

  await onceCaptchaEvent(page, 'solved', handleSolveOnce);
  ```
</CodeGroup>

### removeCaptchaEventListener

```typescript theme={null}
import { removeCaptchaEventListener } from '@intuned/runtime';
import type { Page } from 'playwright';
import type { Captcha, CaptchaStatus } from '@intuned/runtime';

export declare function removeCaptchaEventListener(
  page: Page,
  status: CaptchaStatus,
  f: (captcha: Captcha) => Promise<void> | void
): Promise<void>;
```

Remove a previously registered CAPTCHA callback.

Unsubscribe a callback registered using `onCaptchaEvent` or `onceCaptchaEvent`. You must pass the same page, status, and callback function that were used to register the callback.

<br />

**Parameters**

<ParamField type="Page">
  Playwright page object.
</ParamField>

<ParamField type="CaptchaStatus">
  The CAPTCHA status that the listener was registered for.
</ParamField>

<ParamField type="(captcha: Captcha) => any">
  The callback function that was originally registered. Must be the same function reference.
</ParamField>

<br />

**Returns**

Returns `Promise<void>`. The function unsubscribes the callback and resolves immediately.

<br />

**Raises**

* `RuntimeError` — Thrown when the callback cannot be removed or the page context is invalid.

**Example**

<CodeGroup>
  ```typescript Subscribe and unsubscribe theme={null}
  import { onCaptchaEvent, removeCaptchaEventListener, type Captcha } from '@intuned/runtime';
  import type { Page } from 'playwright';

  async function handleCaptchaEvent(captcha: Captcha): Promise<void> {
    console.log(`Captcha status: ${captcha.status}`);
  }

  // Subscribe to CAPTCHA updates
  await onCaptchaEvent(page, 'solved', handleCaptchaEvent);

  // Later, unsubscribe
  await removeCaptchaEventListener(page, 'solved', handleCaptchaEvent);
  ```
</CodeGroup>

**Note:** The callback function reference must match exactly. Anonymous functions cannot be removed. Callbacks registered with `onceCaptchaEvent` are automatically removed after firing.

## Best practices

* Use `withWaitForCaptchaSolve` when a navigation, submit, or click may trigger the challenge.
* Use `waitForCaptchaSolve` only after the page has already settled and the CAPTCHA is already present.
* Set timeout values high enough for real challenges. `60_000` to `120_000` ms is a safer default than `10_000` ms for production flows.
* Adjust the settle duration for the wait before checking status. It resets when CAPTCHAs are detected.
* Leave `waitForNetworkSettled` enabled unless you have a reason to skip it.
* Subscribe to CAPTCHA updates using `onCaptchaEvent` or `onceCaptchaEvent` for telemetry and monitoring.
* Store callback function references if you need to unsubscribe later.

## Type reference

### `Captcha`

```typescript theme={null}
import type { Captcha, CaptchaStatus, CaptchaError } from '@intuned/runtime';

export type Captcha = {
  id: string;
  tabId: number;
  type: string;
  status: CaptchaStatus;
  retryCount?: number;
  error?: CaptchaError;
};
```

**Properties**

<ParamField type="string">
  Unique identifier for the CAPTCHA observation.
</ParamField>

<ParamField type="number">
  Browser tab ID where the CAPTCHA was detected.
</ParamField>

<ParamField type="string">
  CAPTCHA provider type, such as `recaptcha`, `hcaptcha`, or `cloudflare`.
</ParamField>

<ParamField type={<a href="#captchastatus">CaptchaStatus</a>}>
  Current solving state.
</ParamField>

<ParamField type="number">
  Number of solve attempts made.
</ParamField>

<ParamField type={<a href="#captchaerror">CaptchaError</a>}>
  Error details when `status` is `error`, otherwise `undefined`.
</ParamField>

### `CaptchaStatus`

```typescript theme={null}
import type { CaptchaStatus } from '@intuned/runtime';

export type CaptchaStatus = "attached" | "solving" | "solved" | "error" | "detached";
```

**Values**

* **`attached`** — CAPTCHA element detected in the page. Use this to know when a challenge appears.
* **`solving`** — Solver is actively processing the CAPTCHA.
* **`solved`** — CAPTCHA solved successfully. Resume your workflow.
* **`error`** — Solver failed. Check the `error` field for details.
* **`detached`** — CAPTCHA element removed from the page. Treat as cancelled.

### `CaptchaError`

```typescript theme={null}
import type { CaptchaError, CaptchaErrorCode } from '@intuned/runtime';

export type CaptchaError = {
  code: CaptchaErrorCode;
  error?: any;
};
```

**Properties**

<ParamField type={<a href="#captchaerrorcode">CaptchaErrorCode</a>}>
  Error code indicating the type of failure.
</ParamField>

<ParamField type="any">
  Additional error details, if available.
</ParamField>

### `CaptchaErrorCode`

```typescript theme={null}
import type { CaptchaErrorCode } from '@intuned/runtime';

export type CaptchaErrorCode =
  | "HIT_LIMIT"
  | "MAX_RETRIES"
  | "UNEXPECTED_ERROR"
  | "UNEXPECTED_SERVER_RESPONSE";
```

**Values**

* **`HIT_LIMIT`** — Reached billing limits for CAPTCHA solves. See [Plans and billing](/docs/main/05-references/plans-and-billing) for details on limits and upgrading.
* **`MAX_RETRIES`** — Exceeded maximum retry attempts specified in [`settings.maxRetries`](/docs/main/02-features/stealth-mode-captcha-solving-proxies#settings-reference).
* **`UNEXPECTED_ERROR`** — An unexpected error occurred while solving. This is a solver error and not related to your automation.
* **`UNEXPECTED_SERVER_RESPONSE`** — The solver received an unexpected response. This is a solver error and not related to your automation.


# extendPayload
Source: https://intunedhq.com/docs/main/05-references/runtime-sdk-typescript/extend-payload



```typescript theme={null}
import { extendPayload } from '@intuned/runtime';

export declare function extendPayload(
    payload: Payload | Payload[]
): void;
```

In the context of a job, extends the job execution with the provided payloads after the current payload execution succeeds.

Will implicitly call [`extendTimeout`](./extend-timeout).

<br />

**Parameters**

<ParamField type={<span><a href="#payload">Payload</a> | <a href="#payload">Payload</a>[]</span>}>
  The payload or list of payloads to extend the job with.
</ParamField>

## `Payload`

```typescript theme={null}
export interface Payload {
  api: string;
  parameters: Record<string, any>;
}
```

**Properties**

<ParamField type="string">
  The name of the API to extend.
</ParamField>

<ParamField type="Record<string, any>">
  The parameters to pass to the API.
</ParamField>


# extendTimeout
Source: https://intunedhq.com/docs/main/05-references/runtime-sdk-typescript/extend-timeout



```typescript theme={null}
import { extendTimeout } from '@intuned/runtime';

function extendTimeout(): void
```

Resets the timeout timer for the current attempt. Use this when your automation needs more time than the configured timeout allows—for example, when processing a large dataset or waiting for slow network responses.

When called, the attempt gets a fresh timeout period equal to the original timeout duration. This function doesn't block execution.

<Note>
  This function only works in the context of a Job. It has no effect when running automations directly via the API.
</Note>

**Returns**

Returns `void`.

## Related

* [extendPayload](/docs/main/05-references/runtime-sdk-typescript/extend-payload) — Add additional payloads to the current job execution.


# getAuthSessionParameters
Source: https://intunedhq.com/docs/main/05-references/runtime-sdk-typescript/get-auth-session-parameters



```typescript theme={null}
import { getAuthSessionParameters } from '@intuned/runtime';

async function getAuthSessionParameters(): Promise<any>
```

Returns the parameters used to create the current AuthSession. Use this to access credentials or configuration values that were provided when the AuthSession was created.

This function only works with Credentials-based AuthSessions. It allows your automation to access the original parameters (like usernames, API keys, or other identifiers) without hardcoding them.

**Returns**

Returns `Promise<any>` — The parameters object used to create the AuthSession.

**Throws**

* `Error` — Thrown if the project doesn't use AuthSessions.
* `Error` — Thrown if the AuthSession is Recorder-based (not Credentials-based).
* `Error` — Thrown if the AuthSession is Runtime-based.

## Example: branching on a login flow

When a single site exposes more than one login flow (for example, separate provider and patient logins on the same portal), include a discriminator parameter when the AuthSession is created and read it back inside `check` to validate the correct post-login state.

```typescript auth-sessions/create.ts theme={null}
import { BrowserContext, Page } from "playwright";
import { goToUrl } from "@intuned/browser";

export interface CreateParams {
  loginType: "provider" | "patient";
  username: string;
  password: string;
}

export default async function create(
  params: CreateParams,
  page: Page,
  context: BrowserContext
) {
  await goToUrl({ page, url: "https://portal.example.com/login" });
  await page.getByPlaceholder("Username").fill(params.username);
  await page.getByPlaceholder("Password").fill(params.password);
  await page.getByRole("button", { name: "Sign in" }).click();

  const dashboardPath =
    params.loginType === "provider" ? "/provider/dashboard" : "/patient/home";
  await page.waitForURL(`https://portal.example.com${dashboardPath}`);
}
```

```typescript auth-sessions/check.ts theme={null}
import { BrowserContext, Page } from "playwright";
import { goToUrl } from "@intuned/browser";
import { getAuthSessionParameters } from "@intuned/runtime";

export default async function check(
  page: Page,
  context: BrowserContext
): Promise<boolean> {
  const params = (await getAuthSessionParameters()) as {
    loginType: "provider" | "patient";
  };

  const path =
    params.loginType === "provider" ? "/provider/dashboard" : "/patient/home";
  await goToUrl({ page, url: `https://portal.example.com${path}` });

  return !page.url().includes("/login");
}
```

When the AuthSession is created with `parameters.loginType: "provider"`, both `create` and `check` follow the provider flow; passing `"patient"` switches both to the patient flow.

## Related

* [AuthSessions](/docs/main/02-features/auth-sessions) — Learn about authentication session management.
* [Multiple login flows on the same site](/docs/main/02-features/auth-sessions#usage-patterns) — Usage pattern this example supports.


# Non-retryable errors
Source: https://intunedhq.com/docs/main/05-references/runtime-sdk-typescript/non-retryable-error



Use `NonRetryableError` when retrying an automation cannot succeed without changing its input or configuration.

```typescript theme={null}
import { NonRetryableError } from "@intuned/runtime";

export default async function automation(params: { accountId?: string }) {
  if (!params.accountId) {
    throw new NonRetryableError("accountId is required");
  }

  // Continue the automation.
}
```

The error accepts a single message argument. When it is thrown:

* The current attempt fails.
* Intuned skips any remaining configured attempts for that automation phase.
* During AuthSession:Validate runs with auto-recreate enabled, if the error occurs during the first checks, the auth session will be recreated.


# Overview
Source: https://intunedhq.com/docs/main/05-references/runtime-sdk-typescript/overview



The Intuned Runtime SDK for TypeScript (`@intuned/runtime`) is a required dependency for all Intuned projects. It provides:

1. Runtime setup and configuration for Playwright that integrates with the Intuned platform.
2. The Intuned CLI for developing, testing, and deploying automations locally.
3. Helper functions that give your automations access to Intuned platform features.

All Intuned projects require the Runtime SDK as a dependency. When you deploy a project, the latest version of the Runtime SDK is used.

## Reference

* [persistentStore](./persistent-store) — Key-value store that persists across runs.
* [attemptStore](./attempt-store) — Key-value store scoped to the current attempt.
* [extendPayload](./extend-payload) — Add payloads dynamically during job execution.
* [extendTimeout](./extend-timeout) — Reset the timeout timer for the current attempt.
* [getAuthSessionParameters](./get-auth-session-parameters) — Retrieve parameters of the current AuthSession.
* [CAPTCHA helpers](./captcha-helpers) — Wait for CAPTCHAs to be solved and subscribe to status updates.
* [Non-retryable errors](./non-retryable-error) — Stop a failed run without executing its remaining retries.
* [CLI reference](../cli) — Command-line interface for developing, testing, and deploying automations.


# persistentStore
Source: https://intunedhq.com/docs/main/05-references/runtime-sdk-typescript/persistent-store



```typescript theme={null}
import { persistentStore } from '@intuned/runtime';

persistentStore: PersistentStore
```

A persistent key-value store that maintains data across different API executions. This allows you to share data between different runs of your Projects.

**Constraints:**

* Keys must be at least 1 character long.
* Keys cannot contain `:` (colon) or `#` (hash) characters.
* Data stored must be JSON-serializable.

## `PersistentStore`

```typescript theme={null}
interface PersistentStore {
  get(key: string): Promise<any>;
  set(key: string, value: any): Promise<void>;
}
```

## Method reference

### get

```typescript theme={null}
get(key: string): Promise<any>
```

Retrieve a value from the persistent store.

**Parameters**

<ParamField type="string">
  The key to retrieve the value for. Must be at least 1 character long and cannot contain `:` or `#` characters.
</ParamField>

**Returns**

Returns a Promise that resolves to the value associated with the key, or `undefined` if not found.

**Throws**

* `ZodError` — Thrown if the key is invalid (empty or contains forbidden characters).

### set

```typescript theme={null}
set(key: string, value: any): Promise<void>
```

Store a value in the persistent store.

**Parameters**

<ParamField type="string">
  The key to set the value for. Must be at least 1 character long and cannot contain `:` or `#` characters.
</ParamField>

<ParamField type="any">
  The value to store. Can be any JSON-serializable type (object, array, string, number, boolean, null).
</ParamField>

**Returns**

Returns a Promise that resolves when the value is successfully stored.

**Throws**

* `ZodError` — Thrown if the key is invalid (empty or contains forbidden characters).

## Related

* [KV cache recipe](/docs/main/01-learn/recipes/kv-cache) — Examples and patterns for using key-value stores.
* [attemptStore](/docs/main/05-references/runtime-sdk-typescript/attempt-store) — Store data scoped to the current attempt.


# Sink Retry Behavior
Source: https://intunedhq.com/docs/main/05-references/sink-retry-behavior



## Overview

When Intuned sends data to your configured sink (webhook or S3-compatible storage), temporary failures are automatically retried with exponential backoff. This reference documents Intuned's internal retry behavior for different sink types.

<Note>The retry behavior is system-defined and cannot be modified.</Note>

## Retry Behavior Comparison

| Behavior                | Webhook Sink     | S3-Compatible Sinks |
| ----------------------- | ---------------- | ------------------- |
| **Total Retry Window**  | 1 hour           | 10 minutes          |
| **Initial Retry Delay** | 10 seconds       | 10 seconds          |
| **Maximum Retry Delay** | 2 minutes        | 2 minutes           |
| **Backoff Coefficient** | 2x (exponential) | 2x (exponential)    |

## Retry Timeline Example

| Attempt (Delay) | Cumulative Time | Webhook          | S3-Compatible     |
| --------------- | --------------- | ---------------- | ----------------- |
| 1 (Immediate)   | 0s              | ✓                | ✓                 |
| 2 (+10s)        | 10s             | ✓                | ✓                 |
| 3 (+20s)        | 30s             | ✓                | ✓                 |
| 4 (+40s)        | 1m 10s          | ✓                | ✓                 |
| 5 (+1m 20s)     | 2m 30s          | ✓                | ✓                 |
| 6 (+2m)         | 4m 30s          | ✓                | ✓                 |
| 8 (+2m)         | 8m 30s          | ✓                | ✓                 |
| 7 (+2m)         | 6m 30s          | ✓                | ✓                 |
| 9 (+2m)         | 10m 30s         | ✓                | Stops (10m limit) |
| ...             | ...             | ✓                | —                 |
| Final           | 1h              | Stops (1h limit) | —                 |


# Enterprise Services
Source: https://intunedhq.com/docs/main/06-resources/enterprise-services



We've spent two years building browser automations for companies across e-commerce, insurance, healthcare, government, and more. One thing we heard consistently: "Can you just do it for us?"

That's exactly what Enterprise Services is. Our solution engineers—powered by [Intuned Agent](/docs/02-intuned-agent/overview)—build and maintain browser automation projects for you. You tell us what data you need or what workflows to automate, and we handle the rest: building, deploying, monitoring, and fixing.

The same AI agent that's available to all Intuned users is what our team uses internally. Enterprise Services adds human expertise on top—for complex integrations, high-volume deployments, and cases where you'd rather focus on your product than your scrapers.

## What we handle

### Rapid scraper development

Deploy scrapers across hundreds of websites. Our team helps you:

* Build and test scrapers at scale using Intuned Agent
* Handle complex authentication, anti-bot measures, and CAPTCHAs
* Configure [Jobs](/docs/02-features/jobs-batched-executions) for scheduled execution and result delivery
* Maintain scraper reliability with [self-healing projects](/docs/02-intuned-agent/self-healing-projects)

### Automation development and maintenance

End-to-end browser automation projects, fully managed:

* Custom automation design and implementation
* Ongoing maintenance—site changes, broken selectors, new requirements
* [AuthSession](/docs/02-features/auth-sessions) setup for login-protected workflows
* Monitoring and rapid response to production issues

### Premium support

Priority assistance from our engineering team:

* Dedicated support channels (Slack and Teams)
* Direct access to senior engineers
* Custom training sessions for your team

## Get started

<CardGroup>
  <Card title="Schedule a consultation" icon="calendar" href="https://cal.com/team/intuned/get-started">
    Talk to our team about your use case
  </Card>

  <Card title="Contact support" icon="comments" href="/docs/06-resources/help-and-support">
    Reach out to our support team
  </Card>
</CardGroup>


# Help and Support
Source: https://intunedhq.com/docs/main/06-resources/help-and-support



## Email

Email us [here](mailto:founders@intunedhq.com)

## Meeting

[Schedule a meeting](https://cal.com/team/intuned/get-started) with us to discuss your use case and how we can help you. Its free to try!

## Support

Email us about any issues [here](mailto:support@intunedhq.com)

## Community

Join our [Discord community](https://discord.gg/cRAtnKH4jf) to ask questions, share your projects, and get help from the Intuned team and other users.


# Privacy policy
Source: https://intunedhq.com/docs/main/06-resources/privacy-policy



Effective date: 05/03/2024

1. Introduction

Welcome to The Metrics Shop Inc.

The Metrics Shop Inc. (“us”, “we", or “our”) operates [https://www.intuned.io/](https://www.intuned.io/) (hereinafter referred to as “Service”).

Our Privacy Policy governs your visit to [https://www.intuned.io/](https://www.intuned.io/) and [https://www.intunedhq.com/](https://www.intunedhq.com/), and explains how we collect, safeguard and disclose information that results from your use of our Service.

We use your data to provide and improve Service. By using Service, you agree to the collection and use of information in accordance with this policy. Unless otherwise defined in this Privacy Policy, the terms used in this Privacy Policy have the same meanings as in our Terms and Conditions.

Our Terms and Conditions (“Terms”) govern all use of our Service and together with the Privacy Policy constitutes your agreement with us (“agreement”).

2. Definitions

SERVICE means the [https://www.app.intuned.io/](https://www.app.intuned.io/) website operated by The Metrics Shop Inc.

PERSONAL DATA means data about a living individual who can be identified from those data (or from those and other information either in our possession or likely to come into our possession).

USAGE DATA is data collected automatically either generated by the use of Service or from Service infrastructure itself (for example, the duration of a page visit).

COOKIES are small files stored on your device (computer or mobile device).

DATA CONTROLLER means a natural or legal person who (either alone or jointly or in common with other persons) determines the purposes for which and the manner in which any personal data are, or are to be, processed. For the purpose of this Privacy Policy, we are a Data Controller of your data.

DATA PROCESSORS (OR SERVICE PROVIDERS) means any natural or legal person who processes the data on behalf of the Data Controller. We may use the services of various Service Providers in order to process your data more effectively.

DATA SUBJECT is any living individual who is the subject of Personal Data.

THE USER is the individual using our Service. The User corresponds to the Data Subject, who is the subject of Personal Data.‍

3. Information Collection and Use

We collect several different types of information for various purposes to provide and improve our Service to you.

4. Types of Data Collected

Personal Data

While using our Service, we may ask you to provide us with certain personally identifiable information that can be used to contact or identify you (“Personal Data”). Personally identifiable information may include, but is not limited to:

(a)  Email address

(b)  First name and last name

(c)  Cookie sand Usage Data

We may use your Personal Data to contact you with newsletters, marketing or promotional materials and other information that may be of interest to you. You may opt out of receiving any, or all, of these communications from us by emailing at [info@intuned.io](mailto:info@intuned.io).

Usage Data

We may also collect information that your browser sends whenever you visit our Service or when you access Service by or through a mobile device (“Usage Data”).

This Usage Data may include information such as your computer's Internet Protocol address (e.g. IP address), browser type, browser version, the pages of our Service that you visit, the time and date of your visit, the time spent on those pages, unique device identifiers and other diagnostic data.

When you access Service with a mobile device, this Usage Data may include information such as the type of mobile device you use, your mobile device unique ID, the IP address of your mobile device, your mobile operating system, the type of mobile Internet browser you use, unique device identifiers and other diagnostic data.

Tracking Cookies Data

We use cookies and similar tracking technologies to track the activity on our Service and we hold certain information.

Cookies are files with a small amount of data which may include an anonymous unique identifier. Cookies are sent to your browser from a website and stored on your device. Other tracking technologies are also used such as beacons, tags and scripts to collect and track information and to improve and analyze our Service.

You can instruct your browser to refuse all cookies or to indicate when a cookie is being sent. However, if you do not accept cookies, you may not be able to use some portions of our Service.

Examples of Cookies we use:

(a)  Session Cookies: We use Session Cookies to operate our Service.

(b)  Preference Cookies: We use Preference Cookies to remember your preferences and various settings.

(c)  Security Cookies: We use Security Cookies for security purposes.

(d)  Advertising Cookies: Advertising Cookies are used to serve you with advertisements that may be relevant to you and your interests.

5. Use of Data

The Metrics Shop Inc. uses the collected data for various purposes:

(a)  to provide and maintain our Service;

(b)  to notify you about changes to our Service;

(c)  to allow you to participate in interactive features of our Service when you choose to do so;

(d)  to provide customer support;

(e)  to gather analysis or valuable information so that we can improve our Service;

(f)   to monitor the usage of our Service;

(g)  to detect, prevent and address technical issues;

(h)  to fulfill any other purpose for which you provide it;

(i)    to carry out our obligations and enforce our rights arising from any contracts entered into between you and us, including for billing and collection;

(j)    to provide you with notices about your account and/or subscription, including expiration and renewal notices, email-instructions, etc.;

(k)  to provide you with news, special offers and general information about other goods, services and events which we offer that are similar to those that you have already purchased or enquired about unless you have opted not to receive such information;

(l)    in any other way we may describe when you provide the information;

(m) for any other purpose with your consent.

6. Retention of Data

We will retain your Personal Data only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use your Personal Data to the extent necessary to comply with our legal obligations (for example, if we are required to retain your data to comply with applicable laws), resolve disputes, and enforce our legal agreements and policies.

We will also retain Usage Data for internal analysis purposes. Usage Data is generally retained for a shorter period, except when this data is used to strengthen the security or to improve the functionality of our Service, or we are legally obligated to retain this data for longer time periods.

7. Transfer of Data

Your information, including Personal Data, may be transferred to – and maintained on – computers located outside of your state, province, country or other governmental jurisdiction where the data protection laws may differ from those of your jurisdiction.

If you are located outside United States and choose to provide information to us, please note that we transfer the data, including Personal Data, to United States and process it there.

Your consent to this Privacy Policy followed by your submission of such information represents your agreement to that transfer.

The Metrics Shop Inc. will take all the steps reasonably necessary to ensure that your data is treated securely and in accordance with this Privacy Policy and no transfer of your Personal Data will take place to an organization or a country unless there are adequate controls in place including the security of your data and other personal information.

8. Disclosure of Data

We may disclose personal information that we collect, or you provide:

(a)  Disclosure for Law Enforcement.

Under certain circumstances, we may be required to disclose your Personal Data if required to do so by law or in response to valid requests by public authorities.

(b)  Business Transaction.

If we or our subsidiaries are involved in a merger, acquisition or asset sale, your Personal Data may be transferred.

(c)  Other cases. We may disclose your information also:

(i)   to our subsidiaries and affiliates;

(ii)  to contractors, service providers, and other third parties we use to support our business;

(iii)  to fulfill the purpose for which you provide it;

(iv)  for the purpose of including your company’s logo on our website;

(v)  foray other purpose disclosed by us when you provide the information;

(vi)  with your consent in any other cases;

(vii) if we believe disclosure is necessary or appropriate to protect the rights, property, or safety of the Company, our customers, or others.

9. Security of Data

The security of your data is important to us but remember that no method of transmission over the Internet or method of electronic storage is 100% secure. While we strive to use commercially acceptable means to protect your Personal Data, we cannot guarantee its absolute security.

10. Your Data Protection Rights Under General Data Protection Regulation (GDPR)

If you are a resident of the European Union (EU) and European Economic Area (EEA), you have certain data protection rights, covered by GDPR. – See more at [https://eur-lex.europa.eu/eli/reg/2016/679/oj](https://eur-lex.europa.eu/eli/reg/2016/679/oj)

We aim to take reasonable steps to allow you to correct, amend, delete, or limit the use of your Personal Data.

If you wish to be informed what Personal Data we hold about you and if you want it to be removed from our systems, please email us at [info@intuned.io](mailto:info@intuned.io).

In certain circumstances, you have the following data protection rights:

(a)  the right to access, update or to delete the information we have on you;

(b)  the right of rectification. You have the right to have your information rectified if that information is inaccurate or incomplete;

(c)  the right to object. You have the right to object to our processing of your Personal Data;

(d)  the right of restriction. You have the right to request that we restrict the processing of your personal information;

(e)  the right to data portability. You have the right to be provided with a copy of your Personal Data in a structured, machine-readable and commonly used format;

(f)   the right to withdraw consent. You also have the right to withdraw your consent at any time where we rely on your consent to process your personal information;

Please note that we may ask you to verify your identity before responding to such requests. Please note, we may not able to provide Service without some necessary data.

You have the right to complain to a Data Protection Authority about our collection and use of your Personal Data. For more information, please contact your local data protection authority in the European Economic Area (EEA).

11. Your Data Protection Rights under the California Privacy Protection Act (CalOPPA)

CalOPPA is the first state law in the nation to require commercial websites and online services to post a privacy policy. The law’s reach stretches well beyond California to require a person or company in the United States (and conceivable the world) that operates websites collecting personally identifiable information from California consumers to post a conspicuous privacy policy on its website stating exactly the information being collected and those individuals with whom it is being shared, and to comply with this policy. – See more at: [https://consumercal.org/about-cfc/cfc-education-foundation/california-online-privacy-protection-act-caloppa-3/](https://consumercal.org/about-cfc/cfc-education-foundation/california-online-privacy-protection-act-caloppa-3/)

According to CalOPPA we agree to the following:

(a)   users can visit our site anonymously;

(b)   our Privacy Policy link includes the word “Privacy”, and can easily be found on the page specified above on the homepage of our website;

(c)    users will be notified of any privacy policy changes on our Privacy Policy Page;

(d)   users are able to change their personal information by emailing us at [info@intunedhq.com](mailto:info@intunedhq.com).

Our Policy on “Do Not Track” Signals:

We honor Do Not Track signals and do not track, plant cookies, or use advertising when a Do Not Track browser mechanism is in place. Do Not Track is a preference you can set in your web browser to inform websites that you do not want to be tracked.

You can enable or disable Do Not Track by visiting the Preferences or Settings page of your web browser.

12. Service Providers

We may employ third party companies and individuals to facilitate our Service (“Service Providers”), provide Service on our behalf, perform Service-related services or assist us in analyzing how our Service is used.

These third parties have access to your Personal Data only to perform these tasks on our behalf and are obligated not to disclose or use it for any other purpose.

13. Analytics

We may use third-party Service Providers to monitor and analyze the use of our Service.

Google Analytics

Google Analytics is a web analytics service offered by Google that tracks and reports website traffic. Google uses the data collected to track and monitor the use of our Service. This data is shared with other Google services. Google may use the collected data to contextualize and personalize the ads of its own advertising network.

For more information on the privacy practices of Google, please visit the Google Privacy Terms web page: [https://policies.google.com/privacy?hl=en](https://policies.google.com/privacy?hl=en)

We also encourage you to review the Google's policy for safeguarding your data: [https://support.google.com/analytics/answer/6004245](https://support.google.com/analytics/answer/6004245).

PostHog

PostHog is a web analytics service offered by PostHog that tracks and reports website traffic. PostHog uses the data collected to track and monitor the use of our Service. This data is shared with other PostHog services.

For more information on the privacy practices of PostHog, please visit the PostHog Privacy Terms web page: [https://posthog.com/privacy](https://posthog.com/privacy)

14. CI/CD tools

We may use third-party Service Providers to automate the development process of our Service.

GitHub

GitHub is provided by GitHub, Inc.

GitHub is a development platform to host and review code, manage projects, and build software.

For more information on what data GitHub collects for what purpose and how the protection of the data is ensured, please visit GitHub Privacy Policy page: [https://help.github.com/en/articles/github-privacy-statement](https://help.github.com/en/articles/github-privacy-statement).

15. Behavioral Remarketing

The Metrics Shop Inc. uses remarketing services to advertise on third party websites to you after you visited our Service. We and our third-party vendors use cookies to inform, optimize and serve ads based on your past visits to our Service.

Google Ads (AdWords)

Google Ads (AdWords) remarketing service is provided by Google Inc.

You can opt-out of Google Analytics for Display Advertising and customize the Google Display Network ads by visiting the Google Ads Settings page: [http://www.google.com/settings/ads](http://www.google.com/settings/ads)

Google also recommends installing the Google Analytics Opt-out Browser Add-on – [https://tools.google.com/dlpage/gaoptout](https://tools.google.com/dlpage/gaoptout) – for your web browser. Google Analytics Opt-out Browser Add-on provides visitors with the ability to prevent their data from being collected and used by Google Analytics.

For more information on the privacy practices of Google, please visit the Google Privacy Terms web page: [https://policies.google.com/privacy?hl=en](https://policies.google.com/privacy?hl=en)

16. Payments

We may provide paid products and/or services within Service. In that case, we use third-party services for payment processing (e.g. payment processors).

We will not store or collect your payment card details. That information is provided directly to our third-party payment processors whose use of your personal information is governed by their Privacy Policy. These payment processors adhere to the standards set by PCI-DSS as managed by the PCI Security Standards Council, which is a joint effort of brands like Visa, Mastercard, American Express and Discover. PCI-DSS requirements help ensure the secure handling of payment information.

The payment processors we work with are:

PayPal or Braintree:

Their Privacy Policy can be viewed at [https://www.paypal.com/webapps/mpp/ua/privacy-full](https://www.paypal.com/webapps/mpp/ua/privacy-full)

Stripe:

‍Their Privacy Policy can be viewed at: [https://stripe.com/us/privacy](https://stripe.com/us/privacy)

17. Links to Other Sites

Our Service may contain links to other sites that are not operated by us. If you click a third party link, you will be directed to that third party's site. We strongly advise you to review the Privacy Policy of every site you visit.

We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services.

18. Children's Privacy

Our Services are not intended for use by children under the age of 13 (“Children”).

We do not knowingly collect personally identifiable information from Children under 13. If you become aware that a Child has provided us with Personal Data, please contact us. If we become aware that we have collected Personal Data from Children without verification of parental consent, we take steps to remove that information from our servers.

19. Changes to This Privacy Policy

We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page.

We will let you know via email and/or a prominent notice on our Service, prior to the change becoming effective and update “effective date” at the top of this Privacy Policy.

You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page.

20. Contact Us

If you have any questions about this Privacy Policy, please contact us:

By email: [info@intunedhq.com](mailto:info@intunedhq.com).


# Terms of service
Source: https://intunedhq.com/docs/main/06-resources/terms-of-service



Last updated: 05/03/2024

1. Introduction

Welcome to The Metrics Shop Inc. As you have just clicked our Terms of Service, please pause, grab a cup of coffee and carefully read the following pages. It will take you approximately 20 minutes.

These Terms of Service (“Terms”, “Terms of Service”) govern your use of our web pages located at [https://www.intuned.io/](https://www.intuned.io/) and [https://www.intunedhq.com/](https://www.intunedhq.com/)  operated by The Metrics Shop Inc. This defines the agreement between The Metrics Shop Inc.  (“Company”, “Metrics Shop”, “we”, “our”, “us”) with address of 13641 NE 129TH St., Kirkland, WA, 98034, USA, and you or the entity you represent (”Client”, “you”, “your”). This agreement takes effect when you sign up to The Metrics Shop Inc. services (”the Services”, “Service”). If you are using the Services on behalf of an entity you represent to us that you are lawfully able to enter this Agreement on behalf of the client.

Our [Privacy Policy](/docs/main/06-resources/privacy-policy) also governs your use of our Service and explains how we collect, safeguard and disclose information that results from your use of our web pages. Please read it here [https://www.intunedhq.com/privacy](https://www.intunedhq.com/privacy).

Your agreement with us includes these Terms and our Privacy Policy (“Agreements”). You acknowledge that you have read and understood Agreements, and agree to be bound of them.

If you do not agree with (or cannot comply with) Agreements, then you may not use the Service, but please let us know by emailing at [info@intunedhq.com](mailto:info@intunedhq.com) so we can try to find a solution. These Terms apply to all visitors, users and others who wish to access or use Service.

Thank you for being responsible.

2. Communications

By creating an Account on our Service, you agree to subscribe to newsletters, marketing or promotional materials and other information we may send. However, you may opt out of receiving any, or all, of these communications from us by following the unsubscribe link or by emailing at [info@intunedhq.com](mailto:info@intunedhq.com).

3. Purchases

If you wish to purchase any product or service made available through our Service (“Purchase”), you may be asked to supply certain information relevant to your Purchase including, without limitation, your credit card number, the expiration date of your credit card, your billing address, and your shipping information.

You represent and warrant that: (i) you have the legal right to use any credit card(s) or other payment method(s) in connection with any Purchase; and that (ii) the information you supply to us is true, correct and complete.

We may employ the use of third party services for the purpose of facilitating payment and the completion of Purchases. By submitting your information, you grant us the right to provide the information to these third parties subject to our Privacy Policy.

Certain Services will be made available subject to our completion of a successful compliance review process. Such a review may require you to complete a Know Your Customer process, video calls with the Client and any measures that we decide, at our sole discretion, is necessary to approve your use of the Service. You will cooperate with us and provide us with any information reasonably required as a part of the compliance review process.

We reserve the right to refuse or cancel your order at any time for reasons including but not limited to: product or service availability, errors in the description or price of the product or service, error in your order or other reasons.

4. Subscriptions

Some parts of Service are billed on a subscription basis (“Subscription(s)”). You will be billed in advance on a recurring and periodic basis (“Billing Cycle”). Billing cycles are set either on a monthly or annual basis, depending on the type of subscription plan you select when purchasing a Subscription.

At the end of each Billing Cycle, your Subscription will automatically renew under the exact same conditions unless you cancel it or The Metrics Shop Inc. cancels it. You may cancel your Subscription renewal either through your online account management page or by contacting The Metrics Shop Inc. customer support team.

A valid payment method, including credit card or ACH, is required to process the payment for your subscription. You shall provide The Metrics Shop Inc. with accurate and complete billing information including full name, address, state, zip code, telephone number, and a valid payment method information. By submitting such payment information, you automatically authorize The Metrics Shop Inc. to charge all Subscription fees incurred through your account to any such payment instruments.

Should automatic billing fail to occur for any reason, The Metrics Shop Inc. will issue an electronic invoice indicating that you must proceed manually, within a certain deadline date, with the full payment corresponding to the billing period as indicated on the invoice.

5. Free Trial

The Metrics Shop Inc. may, at its sole discretion, offer a Subscription with a free trial for a limited period of time (“Free Trial”).

You may be required to enter your billing information in order to sign up for Free Trial.

If you do enter your billing information when signing up for Free Trial, you will not be charged by The Metrics Shop Inc. until Free Trial has expired. On the last day of Free Trial period, unless you cancelled your Subscription, you will be automatically charged the applicable Subscription fees for the type of Subscription you have selected.

At any time and without notice, The Metrics Shop Inc. reserves the right to (i) modify Terms of Service of Free Trial offer, or (ii) cancel such Free Trial offer.

6. Fee Changes

The Metrics Shop Inc., in its sole discretion and at any time, may modify Subscription fees for the Subscriptions. Any Subscription fee change will become effective at the end of the then-current Billing Cycle.

The Metrics Shop Inc. will provide you with a reasonable prior notice of any change in Subscription fees to give you an opportunity to terminate your Subscription before such change becomes effective.

Your continued use of Service after Subscription fee change comes into effect constitutes your agreement to pay the modified Subscription fee amount.

7. Refunds

Except when required by law, paid Subscription fees are non-refundable.

8. Prohibited Uses

You may use Service only for lawful purposes and in accordance with Terms.

You agree not to use Service: In any way that violates any applicable national or international law or regulation. For the purpose of exploiting, harming, or attempting to exploit or harm minors in any way by exposing them to inappropriate content or otherwise.To transmit, or procure the sending of, any advertising or promotional material, including any “junk mail”, “chain letter,” “spam,” or any other similar solicitation. To impersonate or attempt to impersonate Company, a Company employee, another user, or any other person or entity without their consent. In any way that infringes upon the rights of others, or in any way is illegal, threatening, fraudulent, or harmful, or in connection with any unlawful, illegal, fraudulent, or harmful purpose or activity. To engage in any other conduct that restricts or inhibits anyone’s use or enjoyment of Service or third partys, or which, as determined by us, may harm or offend Company or users of Service or expose them to liability.

Additionally, you agree not to: Use Service in any manner that could disable, overburden, damage, or impair Service or interfere with any other party’s use of Service or any third party, including their ability to engage in real time activities through Service. Use any manual process to monitor or copy any of the material on Service or for any other unauthorized purpose without our prior written consent. Use any device, software, or routine that interferes with the proper working of Service. Introduce any viruses, trojan horses, worms, logic bombs, or other material which is malicious or technologically harmful. Distribute any unlawful content or encourage any unlawful activity. Cause any damage or service disruption to any third party computers or service; Attempt to gain unauthorized access to, interfere with, damage, or disrupt any parts of Service, the server on which Service is stored, or any server, computer, or database connected to Service. Attack Service via a denial-of-service attack or a distributed denial-of-service attack. Take any action that may damage or falsify Company rating. Otherwise attempt to interfere with the proper working of Service.

Finally, it is our sole discretion to suspend your access to use our services immediately upon notice if we determine that:Your use of or registration to the Service: poses a security risk to us or our Service or any third party, may adversely impact us or any of our clients, including by way of causing a user to be blocked from certain websites, networks or services, may subject us, our affiliates, or any third party to liability, or is in breach under any applicable laws or regulations, may be fraudulent, or may disparage or devalue our reputation or goodwill; or you are in breach of this Agreement, including if you are delinquent on payment obligations; or You have violated any of its representations and warranties under this Agreement or any other representation and warranties provided to us associated with your use of the Service.

9. Analytics

We may use third-party Service Providers to monitor and analyze the use of our Service.

Google Analytics

Google Analytics is a web analytics service offered by Google that tracks and reports website traffic. Google uses the data collected to track and monitor the use of our Service. This data is shared with other Google services. Google may use the collected data to contextualise and personalise the ads of its own advertising network.

For more information on the privacy practices of Google, please visit the Google Privacy Terms web page: [https://policies.google.com/privacy?hl=en](https://policies.google.com/privacy?hl=en)

We also encourage you to review the Google's policy for safeguarding your data: [https://support.google.com/analytics/answer/6004245](https://support.google.com/analytics/answer/6004245).

PostHog

PostHog is a web analytics service offered by PostHog that tracks and reports website traffic. PostHog uses the data collected to track and monitor the use of our Service. This data is shared with other PostHog services.

For more information on the privacy practices of PostHog, please visit the PostHog Privacy Terms web page: [https://posthog.com/privacy](https://posthog.com/privacy)

10. No Use By Minors

Service is intended only for access and use by individuals at least eighteen (18) years old. By accessing or using any of Company, you warrant and represent that you are at least eighteen (18) years of age and with the full authority, right, and capacity to enter into this agreement and abide by all of the terms and conditions of Terms. If you are not at least eighteen (18) years old, you are prohibited from both the access and usage of Service.

11. Accounts

When you create an account with us, you guarantee that you are above the age of 18, and that the information you provide us is accurate, complete, and current at all times. Inaccurate, incomplete, or obsolete information may result in the immediate termination of your account on Service.

You are responsible for maintaining the confidentiality of your account and password, including but not limited to the restriction of access to your computer and/or account. You agree to accept responsibility for any and all activities or actions that occur under your account and/or password, whether your password is with our Service or a third-party service. You must notify us immediately upon becoming aware of any breach of security or unauthorized use of your account.

You may not use as a username the name of another person or entity or that is not lawfully available for use, a name or trademark that is subject to any rights of another person or entity other than you, without appropriate authorization. You may not use as a username any name that is offensive, vulgar or obscene.

Each user is allowed to create and use only one user account. Creating or using multiple personal accounts, even with different email addresses, without our written consent, will be considered a breach of this section.

When you contact our support team for assistance with our Platform or Services, you acknowledge that the support team members may access your account to help resolve your issue. The support team will limit their actions on your account to only those necessary to provide the requested support.

We reserve the right to refuse service, terminate accounts, or cancel orders in our sole discretion.

12. Intellectual Property

Service and its original content (excluding Content provided by users), features and functionality are and will remain the exclusive property of The Metrics Shop Inc. and its licensors. Service is protected by copyright, trademark, and other laws of the United States. Our trademarks and trade dress may not be used in connection with any product or service without the prior written consent of The Metrics Shop Inc..

13. Copyright Policy

We respect the intellectual property rights of others. It is our policy to respond to any claim that Content posted on Service infringes on the copyright or other intellectual property rights (“Infringement”) of any person or entity.

If you are a copyright owner, or authorized on behalf of one, and you believe that the copyrighted work has been copied in a way that constitutes copyright infringement, please submit your claim via email to [dmca@intunedhq.com](mailto:dmca@intunedhq.com), with the subject line: “Copyright Infringement” and include in your claim a detailed description of the alleged Infringement as detailed below, under “DMCA Notice and Procedure for Copyright Infringement Claims”

You may be held accountable for damages (including costs and attorneys' fees) for misrepresentation or bad-faith claims on the infringement of any Content found on and/or through Service on your copyright.

14. DMCA Notice and Procedure for Copyright Infringement Claims

You may submit a notification pursuant to the Digital Millennium Copyright Act (DMCA) by providing our Copyright Agent with the following information in writing (see 17 U.S.C 512(c)(3) for further detail):

an electronic or physical signature of the person authorized to act on behalf of the owner of the copyright's interest;

a description of the copyrighted work that you claim has been infringed, including the URL (i.e., web page address) of the location where the copyrighted work exists or a copy of the copyrighted work;

identification of the URL or other specific location on Service where the material that you claim is infringing is located;

your address, telephone number, and email address;

a statement by you that you have a good faith belief that the disputed use is not authorized by the copyright owner, its agent, or the law;

a statement by you, made under penalty of perjury, that the above information in your notice is accurate and that you are the copyright owner or authorized to act on the copyright owner's behalf.

You can contact our Copyright Agent via email at [dmca@intunedhq.com](mailto:dmca@intunedhq.com)

15. Error Reporting and Feedback

You may provide us directly at [info@intunedhq.com](mailto:info@intunedhq.com) with information and feedback concerning errors, suggestions for improvements, ideas, problems, complaints, and other matters related to our Service (“Feedback”). You acknowledge and agree that: (i) you shall not retain, acquire or assert any intellectual property right or other right, title or interest in or to the Feedback; (ii) Company may have development ideas similar to the Feedback; (iii) Feedback does not contain confidential information or proprietary information from you or any third party; and (iv) Company is not under any obligation of confidentiality with respect to the Feedback. In the event the transfer of the ownership to the Feedback is not possible due to applicable mandatory laws, you grant Company and its affiliates an exclusive, transferable, irrevocable, free-of-charge, sub-licensable, unlimited and perpetual right to use (including copy, modify, create derivative works, publish, distribute and commercialize) Feedback in any manner and for any purpose.

16. Links To Other Web Sites

Our Service may contain links to third party web sites or services that are not owned or controlled by The Metrics Shop Inc.

The Metrics Shop Inc. has no control over, and assumes no responsibility for the content, privacy policies, or practices of any third party web sites or services. We do not warrant the offerings of any of these entities/individuals or their websites.

YOU ACKNOWLEDGE AND AGREE THAT THE METRICS SHOP INC. SHALL NOT BE RESPONSIBLE OR LIABLE, DIRECTLY OR INDIRECTLY, FOR ANY DAMAGE OR LOSS CAUSED OR ALLEGED TO BE CAUSED BY OR IN CONNECTION WITH USE OF OR RELIANCE ON ANY SUCH CONTENT, GOODS OR SERVICES AVAILABLE ON OR THROUGH ANY SUCH THIRD PARTY WEB SITES OR SERVICES.

WE STRONGLY ADVISE YOU TO READ THE TERMS OF SERVICE AND PRIVACY POLICIES OF ANY THIRD PARTY WEB SITES OR SERVICES THAT YOU VISIT.

17. Liability and indemnity
    We are not responsible for any illegal actions you might undertake in relation to the use of our Website, Platform, Configuration, or Services that affect third parties (e.g., violation of intellectual property rights, rights to a name or company name, engaging in unfair competition, or breaching terms of third-party websites, applications, and programs).

It is presumed that your usage of these resources is lawful and ethical, and that you have secured all necessary permissions for their use on targeted websites and/or data sources.

We bear no responsibility for the results of the activities conducted using our Website, Platform, Configuration, or Services. Additionally, we are not liable for any third-party services or products that are incorporated into the Platform or any of its functionalities, nor for their operation, or the outcomes of their use.

We do not provide assurances or accept liability for the availability of the Website, Platform, or Services, nor for their performance, reliability, responsiveness, or other performance metrics. We also do not assume responsibility for the functionality or availability of services from other providers that we facilitate for you. We are also not liable for any breaches you commit regarding the terms of use of such services.

You agree to indemnify, defend and hold us, our agents, affiliates, subsidiaries, directors, officers, employees, and applicable third parties harmless from and against any third-party claim, liability, loss, and expense (including damage awards, settlement amounts, and reasonable legal fees), brought against any Indemnified Person(s), arising out of your use of the Website, Platform, Configurations or Services and/or your breach of any of these terms. You acknowledge and agree that each Indemnified Person has the right to assert and enforce its rights under this section directly on its own behalf as a third-party beneficiary.

18. Limitation Of Liability

EXCEPT AS PROHIBITED BY LAW, YOU WILL HOLD US AND OUR OFFICERS, DIRECTORS, EMPLOYEES, AND AGENTS HARMLESS FOR ANY INDIRECT, PUNITIVE, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGE, HOWEVER IT ARISES (INCLUDING ATTORNEYS' FEES AND ALL RELATED COSTS AND EXPENSES OF LITIGATION AND ARBITRATION, OR AT TRIAL OR ON APPEAL, IF ANY, WHETHER OR NOT LITIGATION OR ARBITRATION IS INSTITUTED), WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE, OR OTHER TORTIOUS ACTION, OR ARISING OUT OF OR IN CONNECTION WITH THIS AGREEMENT, INCLUDING WITHOUT LIMITATION ANY CLAIM FOR PERSONAL INJURY OR PROPERTY DAMAGE, ARISING FROM THIS AGREEMENT AND ANY VIOLATION BY YOU OF ANY FEDERAL, STATE, OR LOCAL LAWS, STATUTES, RULES, OR REGULATIONS, EVEN IF COMPANY HAS BEEN PREVIOUSLY ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. EXCEPT AS PROHIBITED BY LAW, IF THERE IS LIABILITY FOUND ON THE PART OF COMPANY, IT WILL BE LIMITED TO THE AMOUNT PAID FOR THE PRODUCTS AND/OR SERVICES, AND UNDER NO CIRCUMSTANCES WILL THERE BE CONSEQUENTIAL OR PUNITIVE DAMAGES. SOME STATES DO NOT ALLOW THE EXCLUSION OR LIMITATION OF PUNITIVE, INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THE PRIOR LIMITATION OR EXCLUSION MAY NOT APPLY TO YOU.

19. Termination

We may terminate or suspend your account and bar access to Service immediately, without prior notice or liability, under our sole discretion, for any reason whatsoever and without limitation, including but not limited to a breach of Terms.

If you wish to terminate your account, you may simply discontinue using Service.

All provisions of Terms which by their nature should survive termination shall survive termination, including, without limitation, ownership provisions, warranty disclaimers, indemnity and limitations of liability.

20. Governing Law

These Terms shall be governed and construed in accordance with the laws of State of Delaware without regard to its conflict of law provisions.

Our failure to enforce any right or provision of these Terms will not be considered a waiver of those rights. If any provision of these Terms is held to be invalid or unenforceable by a court, the remaining provisions of these Terms will remain in effect. These Terms constitute the entire agreement between us regarding our Service and supersede and replace any prior agreements we might have had between us regarding Service.

21. Changes To Service

We reserve the right to withdraw or amend our Service, and any service or material we provide via Service, in our sole discretion without notice. We will not be liable if for any reason all or any part of Service is unavailable at any time or for any period. From time to time, we may restrict access to some parts of Service, or the entire Service, to users, including registered users.

22. Amendments To Terms

We may amend Terms at any time by posting the amended terms on this site. It is your responsibility to review these Terms periodically.

Your continued use of the Platform following the posting of revised Terms means that you accept and agree to the changes. You are expected to check this page frequently so you are aware of any changes, as they are binding on you.

By continuing to access or use our Service after any revisions become effective, you agree to be bound by the revised terms. If you do not agree to the new terms, you are no longer authorized to use Service.

23. Waiver And Severability

No waiver by Company of any term or condition set forth in Terms shall be deemed a further or continuing waiver of such term or condition or a waiver of any other term or condition, and any failure of Company to assert a right or provision under Terms shall not constitute a waiver of such right or provision.

If any provision of Terms is held by a court or other tribunal of competent jurisdiction to be invalid, illegal or unenforceable for any reason, such provision shall be eliminated or limited to the minimum extent such that the remaining provisions of Terms will continue in full force and effect.

24. Acknowledgement

BY USING SERVICE OR OTHER SERVICES PROVIDED BY US, YOU ACKNOWLEDGE THAT YOU HAVE READ THESE TERMS OF SERVICE AND AGREE TO BE BOUND BY THEM.

25. Contact Us

Please send your feedback, comments, requests for technical support:

By email: [info@intunedhq.com](mailto:info@intunedhq.com).


# How Web Tasks work
Source: https://intunedhq.com/docs/main/07-web-tasks/how-it-works

Execution model, reuse semantics, and the rules that govern Web Task runs

This page explains what happens when you call the [Web Tasks API](./overview): how the agent decides between AI and code, how `reuseKey` evolves over time, and the rules that govern concurrent runs.

## Execution model

When you submit a task, Intuned spins up a browser on managed infrastructure, then runs an agent that mixes AI exploration with generated code.

```
Submit task → load reusable code (if reuseKey)
            → agent explores / runs code
            → write or refine code (if reuseKey)
            → return result
```

1. **Load**: if you provided a `reuseKey`, the system loads any code previously saved under that key. If you didn't, the server mints a fresh `wtr_`-prefixed key and uses it for this run; the key comes back in `reuse.key` so you can reuse the same automation on later calls.
2. **Execute**: the agent runs the task. If saved code exists and works, it is used directly. Otherwise the AI explores the site, decides which parts of the task benefit from being code, and writes that code as it goes.
3. **Save**: on success, the improved code is saved under the key (whether you provided it or the server generated it). On failure, the previous code is left untouched so a bad run can't poison a working automation.
4. **Return**: the result is returned [inline or as a file](./overview#result-delivery), along with `reuse`, `cost`, and timing metadata.

The first run for a new key is the "figuring it out" run: slower, more expensive, AI-heavy. Subsequent runs mostly execute saved code, making them fast, cheap, and consistent. The same applies when the site changes: the AI repairs the code, the next run is back to fast and cheap.

### Rejection

Before the agent starts, the system checks if the task is eligible for Web Tasks. If not, the request is rejected. Common reasons for rejection include:

* Illegal activity, harassment, targeted harm and other violations of our [Terms of Service](/docs/main/06-resources/terms-of-service).
* Tasks that are not web automation (for example, "Write a poem about the sea").
* Tasks that are impossible, incoherent or self-contradictory.

### Model selection

Every run uses an Anthropic model, controlled by the `model` request field: `haiku` (default), `sonnet`, or `opus`. Heavier models cost more but help on harder tasks, such as complex multi-step flows and fiddly extraction. Most repeat runs that just execute saved code work well with `haiku`.

## Reuse rules

### Same key, same automation

A `reuseKey` identifies one reusable automation. Two runs with the same key share code, **regardless of `startUrl` or `parameters`**:

```jsonc theme={null}
// Both runs use the same reusable automation: "companies"
{
  "task": "Scrape YC companies",
  "startUrl": "https://www.ycombinator.com/companies",
  "reuseKey": "companies"
}
{
  "task": "Scrape Wellfound companies",
  "startUrl": "https://wellfound.com/companies",
  "reuseKey": "companies"
}
```

If you want separate automations, use separate keys (`yc_companies`, `wellfound_companies`). The system does not create hidden per-URL memories.

### Failed runs do not update code

If `outcome` is `failed`, `reuse.updated` is always `false`. A failed attempt cannot overwrite a working automation.

## The `reuse` response

When you provide a `reuseKey`, the result includes a `reuse` object describing what happened to the saved code:

| Field                | Meaning                                                                            |
| -------------------- | ---------------------------------------------------------------------------------- |
| `key`                | The `reuseKey` you sent.                                                           |
| `used`               | `true` if existing saved code was loaded and executed.                             |
| `created`            | `true` if this run created the first saved code for this key.                      |
| `updated`            | `true` if this run improved or repaired the saved code. Always `false` on failure. |
| `revision_id`        | ID of the Web Task run whose code is currently saved under this key.               |
| `revision_timestamp` | When the saved code was last updated.                                              |

Three common shapes:

```jsonc theme={null}
// First successful run for a new key
{ "reuse": { "key": "yc_companies", "used": false, "created": true,  "updated": true } }

// Later run, saved code worked as-is
{ "reuse": { "key": "yc_companies", "used": true,  "created": false, "updated": false } }

// Later run that repaired or improved the saved code
{ "reuse": { "key": "yc_companies", "used": true,  "created": false, "updated": true } }
```

## Cost dynamics

Costs are reported per run as `aiUsd` (model spend) and `compute.amount` in hours. Expect cost to follow this shape:

* **First run for a key**: high AI cost, longer compute.
* **Steady-state runs**: low AI cost, short compute.
* **Repair runs**: moderate AI cost when the saved code needs updating (for example, after a site change).

This is the central tradeoff Web Tasks make: pay AI cost up front to generate code, then amortize it across many cheap repeat runs.


# Web Tasks
Source: https://intunedhq.com/docs/main/07-web-tasks/overview

An API for repeatable web tasks, described in natural language and executed with a mix of AI and generated code

<Frame>
  <iframe />
</Frame>

## What are Web Tasks?

The **Web Tasks API** lets you describe a web task in plain English and have it executed for you. This can be:

* Scraping a directory.
* Filling a multi-page form.
* Downloading a batch of files.
* Pulling data out of a table.

You send a task description, an optional starting URL, any parameters it needs, and the shape of the output you want back. The API runs the task and returns the result.

Under the hood, the AI decides which parts of the task benefit from being code, writes that code on the fly, and reuses it on later runs. Across repeat calls with the same [`reuseKey`](#reuse-and-the-reusekey), executions get faster, cheaper, and more consistent, because each run executes saved code instead of re-exploring the site from scratch.

Web Tasks are **workspace-scoped**. Unlike [Projects](/docs/main/00-getting-started/how-intuned-works), you don't create, deploy, or maintain anything. You call the API and get a result.

## When to use Web Tasks

Use Web Tasks when:

* The task **benefits from generated code**, such as crawling pages, extracting data across many entities, repeated multi-step actions.
* You'll **run the same task (or variations of it) more than once**. The payoff comes from reuse.
* You want the API to write and maintain the automation for you instead of owning the code yourself.

Use a [Project](/docs/main/00-getting-started/how-intuned-works) instead when:

* You want to run the deterministic code without AI in the loop, or you want to control exactly when AI is involved.
* You want to write, debug, and own the automation code yourself, in the [online IDE](/docs/main/02-features/online-ide) or [locally with the CLI](/docs/main/02-features/local-development-cli).
* You need full visibility into the code and every execution, such as [browser traces, logs, and attempt timeline](/docs/main/02-features/observability-monitoring-logs).
* You want advanced issue monitoring and auto-maintenance via [self-healing projects](/docs/main/02-intuned-agent/self-healing-projects).
* You need platform features like [AuthSessions](/docs/main/02-features/auth-sessions), [Jobs and scheduling](/docs/main/02-features/jobs-batched-executions), or [Sinks](/docs/client-apis/api-reference/sinks/overview).

## Core concepts

### Start and result

Web Tasks use a two-call lifecycle, same as other long-running Intuned APIs:

* [`POST /{workspaceId}/web-tasks/start`](/docs/client-apis/api-reference/webtasks/web-task--start) - submit the task. Returns a `webTaskId` immediately.
* [`GET /{workspaceId}/web-tasks/{webTaskId}/result`](/docs/client-apis/api-reference/webtasks/web-task--result) - poll for status and result.

### Request fields

| Field          | Required | Description                                                                                                                                             |
| -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `task`         | Yes      | Natural-language instruction describing the task.                                                                                                       |
| `startUrl`     | No       | Optional hint telling the agent where to start in the browser. If omitted, the agent decides from the task description.                                 |
| `parameters`   | No       | Structured inputs that may change between runs (for example, `{ "batch": "S24" }`).                                                                     |
| `outputSchema` | No       | JSON Schema (draft 2020-12) for the desired result shape. If omitted, the system infers a format from the task.                                         |
| `reuseKey`     | No       | Label that ties this run to a reusable automation. See [Reuse and the reuseKey](#reuse-and-the-reusekey). If omitted, the server generates one for you. |
| `model`        | No       | Model the agent runs with: `haiku`, `sonnet`, or `opus`. Defaults to `haiku`. Heavier models help on harder tasks at higher cost.                       |

### Status model

A Web Task moves through these states:

* `pending`: created, not yet executing.
* `started`: executing.
* `completed`: finished. The response includes an `outcome` of `success` or `failed`.

Failed tasks are still `completed`, with a different outcome distinguishing them (reference the [Example response](#example-responses) below). Failed runs never update saved reusable code.

### Result delivery

The server decides how to return the result:

* **Inline**: small outputs (booleans, short text, small arrays, small objects) come back directly in the response under `result.data`.
* **File**: large outputs come back as a downloadable URL under `result.file`, often with a `preview` (row count and a small sample). File URLs are signed and expire after 7 days.

You don't choose the mode upfront.

### Cost

Final responses include cost when available:

* `cost.aiUsd`: AI/model cost in dollars.
* `cost.compute`: runtime compute usage in hours.

## Reuse and the `reuseKey`

`reuseKey` is the mechanism that makes Web Tasks get cheaper and faster over time.

**With a `reuseKey`**, the system loads any code saved under that key and tries to run it. If the existing code works, it returns the result. If not, the AI repairs or updates the code. On success, the improved code is saved back under the same key. On failure, the previous code is left untouched.

**Without a `reuseKey`**, the server still creates a reusable bundle for the run. It auto-generates a `wtr_`-prefixed key, saves code under it, and returns it on the result as `reuse.key`. Pass that key on later calls when you want to reuse the same automation.

Same `reuseKey` means **the same reusable automation**, regardless of URL or parameters. If you want different automations, use different keys (`yc_companies`, `wellfound_companies`, `shopify_products`).

For the full execution model and reuse rules, see [How Web Tasks work](./how-it-works).

## Example

The simplest path is the SDK `run()` helper, which submits the task and polls for the result for you. Default poll interval is 5 seconds; you can pass an optional timeout.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { IntunedClient } from "@intuned/client";

  const client = new IntunedClient({
    apiKey: "<YOUR_API_KEY>",
    workspaceId: "<YOUR_WORKSPACE_ID>",
  });

  const result = await client.webTasks.run(
    {
      task: "Scrape companies from the YC directory",
      startUrl: "https://www.ycombinator.com/companies",
      parameters: { batch: "S24" },
      reuseKey: "yc_companies",
    },
    { pollIntervalMs: 5000, timeoutMs: 10 * 60 * 1000 },
  );

  console.log(result.status, result.outcome);
  ```

  ```python Python theme={null}
  from intuned_client import IntunedClient
  from intuned_client.models import WebTasksStartRequestBody

  with IntunedClient(
      workspace_id="<YOUR_WORKSPACE_ID>",
      api_key="<YOUR_API_KEY>",
  ) as client:
      result = client.web_tasks.run(
          body=WebTasksStartRequestBody(
              task="Scrape companies from the YC directory",
              start_url="https://www.ycombinator.com/companies",
              parameters={"batch": "S24"},
              reuse_key="yc_companies",
          ),
          poll_interval_seconds=5.0,
          timeout_seconds=600,
      )
      print(result.status, result.outcome)
  ```
</CodeGroup>

If you can't use the SDK, call the endpoints directly:

```bash theme={null}
# Start a task
curl -X POST "https://api.intuned.io/{workspaceId}/web-tasks/start" \
  -H "x-api-key: $INTUNED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "task": "Scrape companies from the YC directory",
    "startUrl": "https://www.ycombinator.com/companies",
    "parameters": { "batch": "S24" },
    "reuseKey": "yc_companies"
  }'

# Poll for the result
curl "https://api.intuned.io/{workspaceId}/web-tasks/{webTaskId}/result" \
  -H "x-api-key: $INTUNED_API_KEY"
```

### Example responses

<CodeGroup>
  ```json Successful inline response theme={null}
  {
    "webTaskId": "wt_123",
    "status": "completed",
    "outcome": "success",
    "result": {
      "type": "inline",
      "data": [
        {
          "name": "ExampleCo",
          "batch": "S24",
          "url": "https://www.ycombinator.com/companies/exampleco"
        }
      ]
    },
    "reuse": {
      "key": "yc_companies",
      "used": false,
      "created": true,
      "updated": true
    },
    "cost": {
      "aiUsd": 0.42,
      "compute": { "unit": "hours", "amount": 0.023 }
    }
  }
  ```

  ```json Successful file response theme={null}
  {
    "webTaskId": "wt_123",
    "status": "completed",
    "outcome": "success",
    "result": {
      "type": "file",
      "file": {
        "url": "https://files.intuned.io/results/wt_123.json?signature=abc&expires=1700000000",
        "contentType": "application/json",
        "sizeBytes": 204800,
        "expiresAt": "2026-06-01T22:13:20.000Z"
      }
    },
    "reuse": {
      "key": "yc_companies",
      "used": false,
      "created": true,
      "updated": true
    },
    "cost": {
      "aiUsd": 0.42,
      "compute": { "unit": "hours", "amount": 0.023 }
    }
  }
  ```

  ```json Failed response theme={null}
  {
    "webTaskId": "wt_123",
    "status": "completed",
    "outcome": "failed",
    "error": {
      "code": "rejected",
      "message": "Could not open the page at https://www.ycombinator.com/companies. The website may be down."
    },
    "reuse": {
      "key": "yc_companies",
      "used": false,
      "created": false,
      "updated": false
    },
    "cost": {
      "aiUsd": 0.1,
      "compute": { "unit": "hours", "amount": 0.003 }
    }
  }
  ```
</CodeGroup>

## Cost and billing

Web Tasks consume AI credits based on tokens used and Compute Hours based on the execution time of the tasks. Free plans include credits to get started. Track spend on the [Usage page](https://app.intuned.io/usage).

See [Plans and billing](/docs/main/05-references/plans-and-billing) for plan limits and [Usage and billing](/docs/main/02-features/usage-pricing-monitoring-alerting) for tracking spend.

## Limits

* Each workspace can run up to 10 different Web Tasks concurrently ([Contact support](/docs/main/06-resources/help-and-support) if you need a higher limit).
* Same-`reuseKey` runs are serialized: runs that share a `reuseKey` execute one at a time so two runs don't update the same code concurrently. Runs with different keys can run in parallel.
* Failed runs return `outcome: "failed"` with an `error.code`. Possible codes: `workspace-rate-limited`, `unauthenticated`, `no-ai-credits-left`, `timeout`, `rejected`, `internal-error`.

## FAQ

<AccordionGroup>
  <Accordion title="Can Web Tasks bypass CAPTCHAs or bot detection?">
    Yes. Web Tasks support [stealth mode and CAPTCHA
    solving](/docs/main/02-features/stealth-mode-captcha-solving-proxies) and proxies out of the box and will use them when needed.
  </Accordion>

  <Accordion title="Do Web Tasks work with authenticated websites?">
    No, not yet. Web Tasks currently only work with publicly accessible websites. Support for authenticated sessions is coming soon.
  </Accordion>

  <Accordion title="Can I use my existing Claude Code subscription?">
    No. Web Tasks uses their own AI credits system, separate from any Claude
    Code or Anthropic Console subscription.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup>
  <Card title="How Web Tasks work" icon="book" href="./how-it-works">
    Execution model and reuse rules in detail
  </Card>

  <Card title="API reference" icon="server" href="/client-apis/api-reference/webtasks/web-task--start">
    Start and result endpoints
  </Card>

  <Card title="CLI reference" icon="terminal" href="/main/05-references/cli/platform#web-tasks-start">
    Run Web Tasks from the `intuned platform webtasks` commands
  </Card>
</CardGroup>