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

# Creating Functions

> Write and deploy browser automation Functions

<Tip>
  STOP. Do not read past this section until you have read and followed [/quickstart.md](/quickstart.md).

  Goal: produce a reliable SDK automation script. The intended authoring flow is generate then edit, not write browser automation from scratch.

  Do not hand-write SDK code with element IDs, selectors, or form fields from memory. Element IDs, selectors, and field mappings must come from a live `observe()` call, CLI `notte page observe` output, or generated workflow code. Guessed browser targets fail on real pages.

  Use the Notte skill and CLI to inspect the live site, validate actions/selectors, handle auth/session state, and export workflow code with `notte sessions workflow-code` before using SDK docs or SDK code.

  SDK reference pages are for understanding, running, or editing generated workflow code. They are not the starting point for manually authoring the initial browser automation. SDK-first code is guesswork on real-world pages with dynamic selectors, auth state, CAPTCHAs, and anti-bot behavior.
</Tip>

Learn how to write Function scripts and deploy them to Notte's serverless infrastructure.

## Writing a Function

Functions are Python scripts with a `run()` function that serves as the entry point.

### Basic Function

The minimal Function structure:

{/* @sniptest testers/functions/creating/basic_function.py */}

```python hello_function.py theme={null}
def run():
    """A simple function that returns a greeting."""
    return "Hello from Notte Functions!"
```

### Function with Parameters

Accept input parameters:

{/* @sniptest testers/functions/creating/function_with_params.py */}

```python greet_function.py theme={null}
def run(name: str, greeting: str = "Hello"):
    """
    Greet someone by name.

    Args:
        name: The person's name
        greeting: The greeting word (default: "Hello")

    Returns:
        A personalized greeting
    """
    return f"{greeting}, {name}!"
```

### Browser Automation Function

Use Notte SDK for browser automation:

{/* @sniptest testers/functions/creating/browser_automation_function.py */}

```python scraper_function.py theme={null}
from notte_sdk import NotteClient


def run(url: str, selector: str):
    """
    Scrape data from a website.

    Args:
        url: The website URL
        selector: CSS selector for target element

    Returns:
        Extracted data
    """
    client = NotteClient()

    with client.Session() as session:
        session.execute(type="goto", url=url)
        data = session.scrape(instructions=f"Extract content from {selector}")

    return {"url": url, "data": data}
```

## Deploying Functions

### Via SDK

Deploy from Python:

{/* @sniptest testers/functions/creating/deploy_sdk.py */}

```python deploy_sdk.py theme={null}
from notte_sdk import NotteClient

client = NotteClient()

# Deploy function
function = client.Function(path="scraper_function.py", name="Website Scraper", description="Scrapes data from websites")

print(f"Function deployed: {function.function_id}")
print(f"Version: {function.response.latest_version}")
```

### Deployment Options

{/* @sniptest testers/functions/creating/deployment_options.py */}

```python deployment_options.py theme={null}
from notte_sdk import NotteClient

client = NotteClient()

function = client.Function(
    path="my_function.py",
    name="My Function",  # Display name
    description="What this function does",  # Description
    shared=False,  # Private by default
)
```

**Parameters:**

* `workflow_path` (str, required): Path to your Python file
* `name` (str, optional): Function display name
* `description` (str, optional): What the function does
* `shared` (bool, default=False): Whether function is publicly accessible

## Function Requirements

### The Handler

Must have a `run()` function:

{/* @sniptest testers/functions/creating/handler_examples.py */}

```python handler_examples.py theme={null}
# Correct
def run(param1, param2):
    return "result"


# Wrong - different name
def execute(param1, param2):
    return "result"


# Wrong - no function
result = perform_task()
```

### Dependencies

Import Notte SDK and standard libraries:

{/* @sniptest testers/functions/creating/dependencies.py */}

```python dependencies.py theme={null}
# Built-in imports

# Notte SDK

# Third-party (common libraries available)


def run():
    # Your code
    pass
```

**Available packages:**

* `notte-sdk` - Notte SDK
* `requests` - HTTP client
* `pydantic` - Data validation
* Standard Python library
* Most common packages

### Return Values

Return JSON-serializable data:

{/* @sniptest testers/functions/creating/return_values.py */}

```python return_values.py theme={null}
def valid_returns():
    # Valid return types (JSON serializable)
    return "string"
    return 123
    return {"key": "value"}
    return ["item1", "item2"]
    return None


def invalid_returns():
    # Invalid returns (not JSON serializable)
    # return datetime.now()  # datetime not serializable
    # return lambda x: x     # functions not serializable
    pass
```

## Parameter Types

### Supported Types

{/* @sniptest testers/functions/creating/parameter_types.py */}

```python parameter_types.py theme={null}
def run(
    text: str,  # String
    number: int,  # Integer
    decimal: float,  # Float
    flag: bool,  # Boolean
    items: list,  # List
    data: dict,  # Dictionary
    optional: str | None = None,  # Optional
    with_default: int = 10,  # Default value
):
    pass
```

### Type Validation

Use type hints for automatic validation:

{/* @sniptest testers/functions/creating/type_validation.py */}

```python type_validation.py theme={null}
def run(count: int):
    # count is automatically validated as int
    for i in range(count):
        print(i)
```

### Complex Types

Use Pydantic for structured parameters:

{/* @sniptest testers/functions/creating/complex_types.py */}

```python complex_types.py theme={null}
from notte_sdk import NotteClient
from pydantic import BaseModel


class SearchParams(BaseModel):
    url: str
    query: str
    max_results: int = 10


def run(params: SearchParams):
    # params is validated against SearchParams model
    client = NotteClient()
    # Use params.url, params.query, etc.
```

## Environment Variables

Access secrets securely:

{/* @sniptest testers/functions/creating/environment_variables.py */}

```python environment_variables.py theme={null}
import os

from notte_sdk import NotteClient


def run():
    # Access environment variables
    api_key = os.getenv("MY_API_KEY")
    webhook_url = os.getenv("WEBHOOK_URL")

    if not api_key:
        return {"error": "API key not configured"}

    # Use in automation
    client = NotteClient(api_key=api_key)
```

Set environment variables in Console or locally for testing.

## Error Handling

### Graceful Errors

Return error information in results:

{/* @sniptest testers/functions/creating/graceful_errors.py */}

```python graceful_errors.py theme={null}
from notte_sdk import NotteClient


def run(url: str):
    try:
        client = NotteClient()
        with client.Session() as session:
            session.execute(type="goto", url=url)
            data = session.scrape()

        return {"success": True, "data": data}

    except Exception as e:
        return {"success": False, "error": str(e), "error_type": type(e).__name__}
```

### Raising Exceptions

Let Functions fail explicitly:

{/* @sniptest testers/functions/creating/raising_exceptions.py */}

```python raising_exceptions.py theme={null}
def run(url: str):
    if not url.startswith("https://"):
        raise ValueError("URL must use HTTPS")

    # Continue with automation
```

## Testing Locally

### Test Before Deploying

Run your function locally:

{/* @sniptest testers/functions/creating/test_locally.py */}

```python test_locally.py theme={null}
# Define your function (from your scraper_function.py)
def run(url: str, selector: str) -> dict:
    # Your scraping logic here
    return {"url": url, "selector": selector}


# Test with sample parameters
result = run(url="https://example.com", selector=".content")

print(result)
```

### Mock API Calls

Test without deploying:

{/* @sniptest testers/functions/creating/mock_api_calls.py */}

```python mock_api_calls.py theme={null}
from notte_sdk import NotteClient

client = NotteClient()

# Load existing function with decryption key for local execution
function = client.Function(
    function_id="func_abc123",
    decryption_key="your-decryption-key",  # Required for local execution
)

# Run locally (not on cloud)
result = function.run(local=True, url="https://example.com")

print(result.result)
```

## Best Practices

### 1. Document Parameters

Use clear docstrings:

{/* @sniptest testers/functions/creating/document_params.py */}

```python document_params.py theme={null}
def run(url: str, max_retries: int = 3):
    """
    Fetch data from a website with retries.

    Args:
        url: The website URL to scrape
        max_retries: Number of retry attempts on failure (default: 3)

    Returns:
        Dict with 'success' (bool) and 'data' (any) keys
    """
    pass
```

### 2. Return Structured Data

Use consistent return formats:

{/* @sniptest testers/functions/creating/return_structured.py */}

```python return_structured.py theme={null}

def run(url: str):
    try:
        # Perform automation
        data = scrape_url(url)

        return {"success": True, "data": data, "url": url, "timestamp": datetime.now().isoformat()}

    except Exception as e:
        return {"success": False, "error": str(e), "url": url}
```

### 3. Add Logging

Log key steps for debugging:

{/* @sniptest testers/functions/creating/add_logging.py */}

```python add_logging.py theme={null}
from loguru import logger
from notte_sdk import NotteClient


def run(url: str):
    logger.info(f"Starting scrape of {url}")

    client = NotteClient()
    with client.Session() as session:
        logger.info("Session created")
        session.execute(type="goto", url=url)
        logger.info("Page loaded")

        data = session.scrape()
        logger.info(f"Extracted {len(data)} items")

    return data
```

### 4. Set Timeouts

Prevent functions from hanging:

{/* @sniptest testers/functions/creating/set_timeouts.py */}

```python set_timeouts.py theme={null}
from notte_sdk import NotteClient


def run(url: str):
    client = NotteClient()

    # Set session timeout
    with client.Session(idle_timeout_minutes=5) as session:
        session.execute(type="goto", url=url)
        data = session.scrape()

    return data
```

### 5. Validate Inputs

Check parameters before processing:

{/* @sniptest testers/functions/creating/validate_inputs.py */}

```python validate_inputs.py theme={null}
def run(url: str, count: int):
    # Validate inputs
    if not url.startswith("http"):
        return {"error": "Invalid URL format"}

    if count < 1 or count > 100:
        return {"error": "Count must be between 1 and 100"}

    # Proceed with automation
    pass
```

## Examples

### Simple Scraper

{/* @sniptest testers/functions/creating/simple_scraper.py */}

```python simple_scraper.py theme={null}
from notte_sdk import NotteClient


def run(url: str):
    """Scrape page content."""
    client = NotteClient()

    with client.Session() as session:
        session.execute(type="goto", url=url)
        content = session.scrape()

    return content
```

### Form Submission

{/* @sniptest testers/functions/form_submission.py */}

```python form_submission.py theme={null}
from notte_sdk import NotteClient


def run(form_url: str, name: str, email: str, message: str):
    """Submit a contact form."""
    client = NotteClient()

    with client.Session() as session:
        session.execute(type="goto", url=form_url)
        session.execute(type="fill", id="name", value=name)
        session.execute(type="fill", id="email", value=email)
        session.execute(type="fill", id="message", value=message)
        session.execute(type="click", selector="button[type='submit']")

    return {"status": "submitted"}
```

### Data Extraction

{/* @sniptest testers/functions/structured_extraction.py */}

```python structured_extraction.py theme={null}
from notte_sdk import NotteClient
from pydantic import BaseModel


class Product(BaseModel):
    name: str
    price: float
    in_stock: bool


def run(product_url: str):
    """Extract structured product data."""
    client = NotteClient()

    with client.Session() as session:
        session.execute(type="goto", url=product_url)

        product = session.scrape(response_format=Product, instructions="Extract product details")

    return product.model_dump()
```

## Next Steps

<CardGroup cols={2}>
  <Card title="Invocations" icon="play" href="/features/functions/invocations">
    Learn how to call your Functions
  </Card>

  <Card title="Schedules" icon="clock" href="/features/functions/schedules">
    Schedule Functions to run automatically
  </Card>

  <Card title="Management" icon="sliders" href="/features/functions/management">
    Update and manage Functions
  </Card>

  <Card title="Functions Concept" icon="lightbulb" href="/concepts/functions">
    Back to Functions overview
  </Card>
</CardGroup>
