# Technical Documentation: Significant-Gravitas/AutoGPT

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/Significant-Gravitas/AutoGPT)

### Overview (README.md)

<p align="center">
  <a href="https://platform.agpt.co/signup?utm_source=github&amp;utm_medium=referral&amp;utm_campaign=autogpt_readme&amp;utm_content=hero_banner">
    <img src="docs/content/imgs/readme/experts-team.webp" alt="AutoGPT: Your ambition. An AI team behind it. Meet Otto, Maria, Max, and Frankie." width="100%" />
  </a>
</p>

<h1 align="center">AutoGPT — Your team of AI Experts</h1>

<p align="center">
  Bring in specialists for content, sales, research, and recurring work.<br />
  Brief them in plain English. Let <strong>Otto</strong> coordinate.
</p>

<p align="center">
  <a href="https://platform.agpt.co/signup?utm_source=github&amp;utm_medium=referral&amp;utm_campaign=autogpt_readme&amp;utm_content=header_get_started"><strong>Sign up for a trial</strong></a>
  ·
  <a href="#see-the-team-at-work">Watch the demo</a>
  ·
  <a href="https://agpt.co/experts/?utm_source=github&amp;utm_medium=referral&amp;utm_campaign=autogpt_readme&amp;utm_content=header_experts">Meet the Experts</a>
  ·
  <a href="https://agpt.co/pricing/?utm_source=github&amp;utm_medium=referral&amp;utm_campaign=autogpt_readme&amp;utm_content=header_pricing">Pricing</a>
  ·
  <a href="#self-host-autogpt">Self-host</a>
  ·
  <a href="#build-with-us">Contribute</a>
</p>

<p align="center">
  <a href="https://discord.gg/autogpt">
    <img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscord.com%2Fapi%2Fv10%2Finvites%2Fautogpt%3Fwith_counts%3Dtrue&amp;query=%24.approximate_member_count&amp;label=Discord&amp;suffix=%20members&amp;logo=discord&amp;logoColor=white&amp;color=5865F2" alt="Join the AutoGPT Discord community — member count" />
  </a>
  <a href="https://hub.docker.com/r/significantgravitas/autogpt">
    <img src="https://img.shields.io/docker/v/significantgravitas/autogpt?sort=semver&label=Docker%20Hub&color=2496ED&logo=docker&logoColor=white" alt="AutoGPT image on Docker Hub" />
  </a>
</p>

## One conversation. The right expertise.

Tell Otto what's on your plate. It brings in the right Experts, hands over the work, and keeps you up to date. Talk to a specialist directly when you want to get into the details.

Each Expert has a role you can shape with your instructions, context, skills, and connected apps. Start with a ready-made specialist or create your own.

| Meet your team | Put them to work |
| --- | --- |
| **Otto · Head of AI** | Coordinate a job across specialists and bring their work back to you. |
| **Maria · SEO Content Manager** | Turn a target keyword into a content brief and article draft. |
| **Max · Account Executive** | Research prospects, draft outreach, and prepare the next conversation. |
| **Frankie · Executive Assistant** | Prepare meeting context, email drafts, and a digest for the day. |

[Explore the Experts →](https://agpt.co/experts/?utm_source=github&utm_medium=referral&utm_campaign=autogpt_readme&utm_content=team_experts)

## See the team at work

One launch brief to Otto. Maria prepares the article, Max researches prospects, and Frankie prepares the launch review.

https://github.com/user-attachments/assets/83cddd83-00f8-46e7-be9e-e0a7237c7eea

**[Watch the full 45-second demo →](https://agpt.co/videos/otto-team-demo-v1-1080.mp4)** · [Sign up for a trial](https://platform.agpt.co/signup?utm_source=github&utm_medium=referral&utm_campaign=autogpt_readme&utm_content=demo_get_started)

A 15-second highlight of a real run, edited and sped up. In this example, the team prepares the work for review without sending or publishing it.

## From a first brief to a routine

1. **Choose your Expert.** Tell Otto what you need, pick a specialist, or create one for your own way of working.
2. **Brief them on the job.** Share the context, connect the apps they need, and agree on the first outcome.
3. **Review and refine.** Check the work and adjust the instructions. Activate routines for jobs that keep coming back.

You choose which accounts each Expert can access, set spending controls, and follow progress in their activity.

[See how it works →](https://agpt.co/how-it-works/?utm_source=github&utm_medium=referral&utm_campaign=autogpt_readme&utm_content=how_it_works)

## Get started

### AutoGPT Platform

Use the hosted Platform to get your team running without managing the infrastructure or supplying model API keys.

**[Sign up for a trial →](https://platform.agpt.co/signup?utm_source=github&utm_medium=referral&utm_campaign=autogpt_readme&utm_content=platform_get_started)**

The hosted Platform is a paid service. See [current plans, trial terms, and usage pricing](https://agpt.co/pricing/?utm_source=github&utm_medium=referral&utm_campaign=autogpt_readme&utm_content=platform_pricing).

### Self-host AutoGPT

Run AutoGPT on your own infrastructure. You provide model credentials, configure the deployment, and manage its updates and operation.

The quickest way to self-host is the single-container image on
Docker Hub, [`significantgravitas/autogpt`](https://hub.docker.com/r/significantgravitas/autogpt):

```bash
docker run -d \
  --name autogpt \
  --restart unless-stopped \
  --shm-size 2g \
  --ulimit nofile=65536:65536 \
  --log-driver json-file \
  --log-opt max-size=50m \
  --log-opt max-file=5 \
  -p 127.0.0.1:3000:3000 \
  -e AUTOGPT_PUBLIC_URL=http://localhost:3000 \
  -v autogpt-data:/data \
  significantgravitas/autogpt:latest
```

Give the first boot a few minutes, then open
[http://localhost:3000](http://localhost:3000), create your account, and make it
the administrator:

```bash
docker exec autogpt autogpt-admin promote you@example.com
```

Requirements, closing signup, ports, model providers, backups, and upgrades are
covered in the guide:

[Read the single-container guide →](https://agpt.co/docs/platform/self-hosting/single-container)

On Linux, macOS, and Windows, the [installer](https://agpt.co/docs/platform/self-hosting/installer#install)
sets up the same image with one command.

To run each service in its own container with Docker Compose, or to develop from
source, use the [manual self-hosting guide](https://agpt.co/docs/platform/self-hosting/getting-started#manual-setup).

| | Hosted Platform | Self-hosted |
| --- | --- | --- |
| Setup and operations | Managed by AutoGPT | Managed by you |
| Model access | Built in | Bring your own model credentials or a local model |
| Cost | Paid plan and usage | Your infrastructure and model usage |
| Data and infrastructure | Hosted by AutoGPT | Runs on your infrastructure |

See the [license](#license) for the repository's usage terms.

## Workflows when you need more control

Experts can use skills and workflows to do their jobs. The visual builder lets you define repeatable steps, connect tools, and inspect how work runs. Start with a workflow from the marketplace or build your own.

- **[Visual builder](https://agpt.co/product/build/?utm_source=github&utm_medium=referral&utm_campaign=autogpt_readme&utm_content=build):** connect blocks, branch logic, and control the steps.
- **[Marketplace](https://platform.agpt.co/marketplace?utm_source=github&utm_medium=referral&utm_campaign=autogpt_readme&utm_content=marketplace):** discover workflows you can adapt to your work.
- **[Integrations](https://agpt.co/docs/integrations):** connect the tools your team uses.

## Build with us

Explore the code, report a problem, or help improve AutoGPT.

| Resource | Where to go |
| --- | --- |
| Platform source and development setup | [autogpt_platform/](autogpt_platform/README.md) |
| Documentation | [agpt.co/docs](https://agpt.co/docs) |
| Contributing | [CONTRIBUTING.md](CONTRIBUTING.md) |
| Bugs and feature requests | [GitHub Issues](https://github.com/Significant-Gravitas/AutoGPT/issues/new/choose) |
| Community and support | [Discord](https://discord.gg/autogpt) |

## License

| Component | License | What it means |
|---|---|---|
| `autogpt_platform/` | [Polyform Shield](https://polyformproject.org/licenses/shield/1.0.0) | Free for personal and internal business use; cannot be sold as a competing hosted service |
| `classic/` and everything else | [MIT](LICENSE) | Permissive open-source use |

---

## AutoGPT Classic

Looking for the original standalone AutoGPT? Explore [Classic](classic/README.md), [Forge](classic/forge/README.md), or [agbenchmark](https://pypi.org/project/agbenchmark/).

## Contributors

<a href="https://github.com/Significant-Gravitas/AutoGPT/graphs/contributors">
  <img src="https://contrib.rocks/image?repo=Significant-Gravitas/AutoGPT&amp;max=1000&amp;columns=10" alt="AutoGPT contributors" />
</a>

<p align="center">
  <a href="https://platform.agpt.co/signup?utm_source=github&amp;utm_medium=referral&amp;utm_campaign=autogpt_readme&amp;utm_content=footer_get_started"><strong>Sign up for a trial →</strong></a>
</p>

---


[Deutsch](https://zdoc.app/de/Significant-Gravitas/AutoGPT) |
[Español](https://zdoc.app/es/Significant-Gravitas/AutoGPT) |
[français](https://zdoc.app/fr/Significant-Gravitas/AutoGPT) |
[日本語](https://zdoc.app/ja/Significant-Gravitas/AutoGPT) |
[한국어](https://zdoc.app/ko/Significant-Gravitas/AutoGPT) |
[Português](https://zdoc.app/pt/Significant-Gravitas/AutoGPT) |
[Русский](https://zdoc.app/ru/Significant-Gravitas/AutoGPT) |
[中文](https://zdoc.app/zh/Significant-Gravitas/AutoGPT)

---

### Content Index (`docs/content/index.md`)

# AutoGPT Documentation

Welcome to the AutoGPT Documentation.

## What is the AutoGPT Platform?

The AutoGPT Platform is a groundbreaking system that revolutionizes AI utilization for businesses and individuals. It enables the creation, deployment, and management of continuous agents that work tirelessly on your behalf, bringing unprecedented efficiency and innovation to your workflows.

### Key Features

- **Seamless Integration and Low-Code Workflows**: Rapidly create complex workflows without extensive coding knowledge.
- **Autonomous Operation and Continuous Agents**: Deploy cloud-based assistants that run indefinitely, activating on relevant triggers.
- **Intelligent Automation and Maximum Efficiency**: Streamline workflows by automating repetitive processes.
- **Reliable Performance and Predictable Execution**: Enjoy consistent and dependable long-running processes.

## Platform Architecture

The AutoGPT Platform consists of two main components:

### 1. AutoGPT Server

The powerhouse of our platform, containing:

-  **Source Code**: Core logic driving agents and automation processes.
-  **Infrastructure**: Robust systems ensuring reliable and scalable performance.
-  **Marketplace**: A comprehensive marketplace for pre-built agents.

### 2. AutoGPT Frontend

The user interface where you interact with the platform:

-  **Agent Builder**: Design and configure your own AI agents.
-  **Workflow Management**: Build, modify, and optimize automation workflows.
-  **Deployment Controls**: Manage the lifecycle of your agents.
-  **Ready-to-Use Agents**: Select from pre-configured agents.
-  **Agent Interaction**: Run and interact with agents through a user-friendly interface.
-  **Monitoring and Analytics**: Track agent performance and gain insights.

##  Platform Components

### Agents and Workflows

In the platform, you can create highly customized workflows to build agents. An agent is essentially an automated workflow that you design to perform specific tasks or processes. Create customized workflows to build agents for various tasks, including:

- Data processing and analysis
- Task scheduling and management
- Communication and notification systems
- Integration between different software tools
- AI-powered decision making and content generation

### Blocks as Integrations

Blocks represent actions and are the building blocks of your workflows, including:

- Connections to external services
- Data processing tools
- AI models for various tasks
- Custom scripts or functions
- Conditional logic and decision-making components

You can learn more under: [Build your own Blocks](../platform/new_blocks.md)

## Available Language Models

The platform comes pre-integrated with cutting-edge LLM providers:

- OpenAI - https://openai.com/
- Anthropic - https://www.anthropic.com/
- Groq - https://groq.com/
- Llama - https://llamaindex.ai/
- AI/ML API - [https://aimlapi.com/](https://aimlapi.com/?utm_source=autogpt&utm_medium=github&utm_campaign=integration)
  - AI/ML API provides 300+ AI models including Deepseek, Gemini, ChatGPT. The models run at enterprise-grade rate limits and uptimes.

## License Overview 

We've adopted a dual-license approach to balance open collaboration with sustainable development:

- **MIT License**: The majority of the AutoGPT repository remains under this license.
- **Polyform Shield License**: Applies to the new `autogpt_platform` folder.

This strategy allows us to share previously closed-source components, fostering a vibrant ecosystem of developers and users.

## Ready to Get Started?

- Read the [Getting Started docs](https://agpt.co/docs/platform/getting-started) to self-host
- [Join the waitlist](https://agpt.co/waitlist) for the cloud-hosted beta
- [Contribute](contribute/index.md)

---

### Engineering README (`docs/engineering/README.md`)

# Engineering notes

Working documents for the team and the agents that work on AutoGPT: analytics and tracking plans, rollout plans, runbooks, internal references, and design and architecture notes.

Nothing in this folder is on the docs site. The site at agpt.co/docs is built from `docs/home/`, `docs/platform/` and `docs/integrations/`, and shows the pages listed in each folder's `SUMMARY.md`. A page written for people outside the team, whether they use, self-host or contribute to AutoGPT, goes there. Everything else goes here.

These notes are still public. The repository is public, and AutoPilot's documentation search indexes every Markdown file under `docs/`, this folder included.

- Don't list a file from this folder in a `SUMMARY.md`.
- To link to a note from a published page, use its GitHub URL on `dev`. The relative path does not exist on the docs site.

---

### Home README (`docs/home/README.md`)

---
description: Welcome to your team’s developer platform
cover: .gitbook/assets/Banner_image.png
coverY: 56.53835084561286
layout:
  width: wide
  cover:
    visible: true
    size: full
  title:
    visible: false
  description:
    visible: false
  tableOfContents:
    visible: false
  outline:
    visible: false
  pagination:
    visible: false
  metadata:
    visible: true
---

# Developer Platform

<h2 align="center">AutoGPT Documentation</h2>

<p align="center">Create innovative agents that amplify human potential</p>



<table data-view="cards"><thead><tr><th></th><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th><th data-hidden data-card-cover data-type="image">Cover image</th></tr></thead><tbody><tr><td><h4><i class="fa-leaf">:leaf:</i></h4></td><td><strong>AutoGPT Platform</strong></td><td>Get started with the developer platform in 5 minutes.</td><td><a href="https://app.gitbook.com/o/ouZP6hgdu8LtbWil2Hvb/s/iMDOYkb9SC1mijdHzvKF/">AutoGPT Platform</a></td><td><a href=".gitbook/assets/AGPT_Platform.png">AGPT_Platform.png</a></td></tr><tr><td><h4><i class="fa-server">:server:</i></h4></td><td><strong>Integrations</strong></td><td>Browse the blocks and integrations your agents can use.</td><td><a href="https://app.gitbook.com/o/ouZP6hgdu8LtbWil2Hvb/s/InwWrtMw9tc0NCzvPVK4/">Integrations</a></td><td><a href=".gitbook/assets/Integrations.png">Integrations.png</a></td></tr><tr><td><h4><i class="fa-terminal">:terminal:</i></h4></td><td><strong>Contribute</strong></td><td>Browse, test, and implement APIs.</td><td><a href="https://app.gitbook.com/s/x9A8W5T9IXYWENaFbHqO/">Contributing to the Docs</a></td><td><a href=".gitbook/assets/Contribute.png">Contribute.png</a></td></tr></tbody></table>

{% columns %}
{% column valign="middle" %}
<p align="center"><a href="https://github.com/Significant-Gravitas/AutoGPT/blob/master/LICENSE">AutoGPT License on GitHub</a></p>
{% endcolumn %}

{% column %}
<p align="center"><a href="https://agpt.co/docs/platform/self-hosting/single-container">Self-Host with Docker</a></p>
{% endcolumn %}

{% column %}
<p align="center"><a href="https://app.gitbook.com/s/dDGesk9atyMLUMMo4QuI/autogpt-classic/introduction">AutoGPT Classic (unsupported)</a></p>
{% endcolumn %}
{% endcolumns %}

***



<h2 align="center">Join a community of 65,000+ developers</h2>

<p align="center">Join our Discord community or create your first PR in just a few steps.</p>

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th></th><th></th><th></th><th data-hidden data-card-cover data-type="files"></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><h4><i class="fa-discord">:discord:</i></h4></td><td><strong>Discord community</strong></td><td>Join our Discord community to post questions, get help, and share resources with our growing community of over 55,000 members.</td><td><a href="https://www.gitbook.com/" class="button secondary">Join Discord</a></td><td></td><td><a href="https://discord.com/invite/autogpt">https://discord.com/invite/autogpt</a></td></tr><tr><td><h4><i class="fa-github">:github:</i></h4></td><td><strong>GitHub</strong></td><td>Our product is 100% open source and built by developers just like you. Head to our GitHub repository to learn how to submit your first PR.</td><td><a href="https://www.gitbook.com/" class="button secondary">Submit a PR</a></td><td></td><td><a href="https://github.com/Significant-Gravitas/AutoGPT">https://github.com/Significant-Gravitas/AutoGPT</a></td></tr></tbody></table>

---

### Integrations README (`docs/integrations/README.md`)

---
layout:
  width: default
  title:
    visible: true
  description:
    visible: true
  tableOfContents:
    visible: false
  outline:
    visible: true
  pagination:
    visible: true
  metadata:
    visible: true
---

# AutoGPT Blocks Overview

AutoGPT uses a modular approach with various "blocks" to handle different tasks. These blocks are the building blocks of AutoGPT workflows, allowing users to create complex automations by combining simple, specialized components.

{% hint style="info" %}
**Creating Your Own Blocks**

Want to create your own custom blocks? Check out our guides:

* [Build your own Blocks](https://agpt.co/docs/platform/new-blocks) - Step-by-step tutorial with examples
* [Block SDK Guide](https://agpt.co/docs/platform/block-sdk-guide) - Advanced SDK patterns with OAuth, webhooks, and provider configuration
{% endhint %}

Below is a comprehensive list of all available blocks, categorized by their primary function. Click on any block name to view its detailed documentation.

## Basic Operations

| Block Name | Description |
|------------|-------------|
| [Add Memory](block-integrations/basic.md#add-memory) | Add new memories to Mem0 with user segmentation |
| [Add To Dictionary](block-integrations/basic.md#add-to-dictionary) | Adds a new key-value pair to a dictionary |
| [Add To Library From Store](block-integrations/system/library_operations.md#add-to-library-from-store) | Add an agent from the store to your personal library |
| [Add To List](block-integrations/basic.md#add-to-list) | Adds a new entry to a list |
| [Agent Date Input](block-integrations/basic.md#agent-date-input) | Block for date input |
| [Agent Dropdown Input](block-integrations/basic.md#agent-dropdown-input) | Block for dropdown text selection |
| [Agent File Input](block-integrations/basic.md#agent-file-input) | Block for file upload input (string path for example) |
| [Agent Google Drive File Input](block-integrations/basic.md#agent-google-drive-file-input) | Agent-level input for a Google Drive file |
| [Agent Input](block-integrations/basic.md#agent-input) | A block that accepts and processes user input values within a workflow, supporting various input types and validation |
| [Agent Long Text Input](block-integrations/basic.md#agent-long-text-input) | Block for long text input (multi-line) |
| [Agent Number Input](block-integrations/basic.md#agent-number-input) | Block for number input |
| [Agent Output](block-integrations/basic.md#agent-output) | A block that records and formats workflow results for display to users, with optional Jinja2 template formatting support |
| [Agent Short Text Input](block-integrations/basic.md#agent-short-text-input) | Block for short text input (single-line) |
| [Agent Table Input](block-integrations/basic.md#agent-table-input) | Block for table data input with customizable headers |
| [Agent Time Input](block-integrations/basic.md#agent-time-input) | Block for time input |
| [Agent Toggle Input](block-integrations/basic.md#agent-toggle-input) | Block for boolean toggle input |
| [Block Installation](block-integrations/basic.md#block-installation) | Given a code string, this block allows the verification and installation of a block code into the system |
| [Concatenate Lists](block-integrations/basic.md#concatenate-lists) | Concatenates multiple lists into a single list |
| [Dictionary Is Empty](block-integrations/basic.md#dictionary-is-empty) | Checks if a dictionary is empty |
| [File Store](block-integrations/basic.md#file-store) | Downloads and stores a file from a URL, data URI, or local path |
| [Find In Dictionary](block-integrations/basic.md#find-in-dictionary) | A block that looks up a value in a dictionary, list, or object by key or index and returns the corresponding value |
| [Find In List](block-integrations/basic.md#find-in-list) | Finds the index of the value in the list |
| [Flatten List](block-integrations/basic.md#flatten-list) | Flattens a nested list structure into a single flat list |
| [Get All Memories](block-integrations/basic.md#get-all-memories) | Retrieve all memories from Mem0 with optional conversation filtering |
| [Get Latest Memory](block-integrations/basic.md#get-latest-memory) | Retrieve the latest memory from Mem0 with optional key filtering |
| [Get List Item](block-integrations/basic.md#get-list-item) | Returns the element at the given index |
| [Get Store Agent Details](block-integrations/system/store_operations.md#get-store-agent-details) | Get detailed information about an agent from the store |
| [Get Weather Information](block-integrations/basic.md#get-weather-information) | Retrieves weather information for a specified location using OpenWeatherMap API |
| [Human In The Loop](block-integrations/basic.md#human-in-the-loop) | Pause execution for human review |
| [Interleave Lists](block-integrations/basic.md#interleave-lists) | Interleaves elements from multiple lists in round-robin fashion, alternating between sources |
| [List Difference](block-integrations/basic.md#list-difference) | Computes the difference between two lists |
| [List Intersection](block-integrations/basic.md#list-intersection) | Computes the intersection of two lists, returning only elements present in both |
| [List Is Empty](block-integrations/basic.md#list-is-empty) | Checks if a list is empty |
| [List Library Agents](block-integrations/system/library_operations.md#list-library-agents) | List all agents in your personal library |
| [Note](block-integrations/basic.md#note) | A visual annotation block that displays a sticky note in the workflow editor for documentation and organization purposes |
| [Print To Console](block-integrations/basic.md#print-to-console) | A debugging block that outputs text to the console for monitoring and troubleshooting workflow execution |
| [Remove From Dictionary](block-integrations/basic.md#remove-from-dictionary) | Removes a key-value pair from a dictionary |
| [Remove From List](block-integrations/basic.md#remove-from-list) | Removes an item from a list by value or index |
| [Replace Dictionary Value](block-integrations/basic.md#replace-dictionary-value) | Replaces the value for a specified key in a dictionary |
| [Replace List Item](block-integrations/basic.md#replace-list-item) | Replaces an item at the specified index |
| [Reverse List Order](block-integrations/basic.md#reverse-list-order) | Reverses the order of elements in a list |
| [Search Memory](block-integrations/basic.md#search-memory) | Search memories in Mem0 by user |
| [Search Store Agents](block-integrations/system/store_operations.md#search-store-agents) | Search for agents in the store |
| [Slant3D Cancel Order](block-integrations/slant3d/order_status.md#slant3d-cancel-order) | Cancel an order before production starts |
| [Slant3D Create Order](block-integrations/slant3d/order.md#slant3d-create-order) | Order physical 3D-printed parts from Slant3D for manufacturing and delivery |
| [Slant3D Estimate Order](block-integrations/slant3d/order.md#slant3d-estimate-order) | Quote a 3D-printed parts order including shipping by creating an uncharged Slant3D draft |
| [Slant3D Estimate Shipping](block-integrations/slant3d/order.md#slant3d-estimate-shipping) | Estimate delivery costs for physical 3D-printed parts by creating an uncharged Slant3D order draft |
| [Slant3D Filament](block-integrations/slant3d/filament.md#slant3d-filament) | Find available materials and colors for quoting or ordering physical 3D-printed parts from Slant3D |
| [Slant3D Get Orders](block-integrations/slant3d/order_status.md#slant3d-get-orders) | Get all orders for the account |
| [Slant3D Process Order](block-integrations/slant3d/order_status.md#slant3d-process-order) | Submit an approved Slant3D draft to order physical 3D-printed parts for manufacturing and delivery |
| [Slant3D Slicer](block-integrations/slant3d/slicing.md#slant3d-slicer) | Get a live 3D printing quote for physical parts from STL URLs or attached workspace files |
| [Slant3D Tracking](block-integrations/slant3d/order_status.md#slant3d-tracking) | Track order status and shipping |
| [Store Value](block-integrations/basic.md#store-value) | Holds or receives a value and outputs it statically so that it can be used multiple times within the same agent run |
| [Universal Type Converter](block-integrations/basic.md#universal-type-converter) | This block is used to convert a value to a universal type |
| [XML Parser](block-integrations/basic.md#xml-parser) | Parses XML using gravitasml to tokenize and coverts it to dict |
| [Zip Lists](block-integrations/basic.md#zip-lists) | Zips multiple lists together into a list of grouped elements |

## Data Processing

| Block Name | Description |
|------------|-------------|
| [Airtable Create Base](block-integrations/airtable/bases.md#airtable-create-base) | Create or find a base in Airtable |
| [Airtable Create Field](block-integrations/airtable/schema.md#airtable-create-field) | Add a new field to an Airtable table |
| [Airtable Create Records](block-integrations/airtable/records.md#airtable-create-records) | Create records in an Airtable table |
| [Airtable Create Table](block-integrations/airtable/schema.md#airtable-create-table) | Create a new table in an Airtable base |
| [Airtable Delete Records](block-integrations/airtable/records.md#airtable-delete-records) | Delete records from an Airtable table |
| [Airtable Get Record](block-integrations/airtable/records.md#airtable-get-record) | Get a single record from Airtable |
| [Airtable List Bases](block-integrations/airtable/bases.md#airtable-list-bases) | List all bases in Airtable |
| [Airtable List Records](block-integrations/airtable/records.md#airtable-list-records) | List records from an Airtable table |
| [Airtable List Schema](block-integrations/airtable/schema.md#airtable-list-schema) | Get the complete schema of an Airtable base |
| [Airtable Update Field](block-integrations/airtable/schema.md#airtable-update-field) | Update field properties in an Airtable table |
| [Airtable Update Records](block-integrations/airtable/records.md#airtable-update-records) | Update records in an Airtable table |
| [Airtable Update Table](block-integrations/airtable/schema.md#airtable-update-table) | Update table properties |
| [Airtable Webhook Trigger](block-integrations/airtable/triggers.md#airtable-webhook-trigger) | Starts a flow whenever Airtable emits a webhook event |
| [Baas Bot Delete Recording](block-integrations/baas/bots.md#baas-bot-delete-recording) | Permanently delete a meeting's recorded data |
| [Baas Bot Fetch Meeting Data](block-integrations/baas/bots.md#baas-bot-fetch-meeting-data) | Retrieve recorded meeting data |
| [Create Dictionary](block-integrations/data.md#create-dictionary) | Creates a dictionary with the specified key-value pairs |
| [Create List](block-integrations/data.md#create-list) | Creates a list with the specified values |
| [Data For Seo Keyword Suggestions](block-integrations/dataforseo/keyword_suggestions.md#data-for-seo-keyword-suggestions) | Get keyword suggestions from DataForSEO Labs Google API |
| [Data For Seo Related Keywords](block-integrations/dataforseo/related_keywords.md#data-for-seo-related-keywords) | Get related keywords from DataForSEO Labs Google API |
| [Exa Create Import](block-integrations/exa/websets_import_export.md#exa-create-import) | Import CSV data to use with websets for targeted searches |
| [Exa Delete Import](block-integrations/exa/websets_import_export.md#exa-delete-import) | Delete an import |
| [Exa Export Webset](block-integrations/exa/websets_import_export.md#exa-export-webset) | Export webset data in JSON, CSV, or JSON Lines format |
| [Exa Get Import](block-integrations/exa/websets_import_export.md#exa-get-import) | Get the status and details of an import |
| [Exa Get New Items](block-integrations/exa/websets_items.md#exa-get-new-items) | Get items added since a cursor - enables incremental processing without reprocessing |
| [Exa List Imports](block-integrations/exa/websets_import_export.md#exa-list-imports) | List all imports with pagination support |
| [File Read](block-integrations/data.md#file-read) | Reads a file and returns its content as a string, with optional chunking by delimiter and size limits |
| [Google Calendar Read Events](block-integrations/google/calendar.md#google-calendar-read-events) | Retrieves upcoming events from a Google Calendar with filtering options |
| [Google Docs Append Markdown](block-integrations/google/docs.md#google-docs-append-markdown) | Append Markdown content to the end of a Google Doc with full formatting - ideal for LLM/AI output |
| [Google Docs Append Plain Text](block-integrations/google/docs.md#google-docs-append-plain-text) | Append plain text to the end of a Google Doc (no formatting applied) |
| [Google Docs Create](block-integrations/google/docs.md#google-docs-create) | Create a new Google Doc |
| [Google Docs Delete Content](block-integrations/google/docs.md#google-docs-delete-content) | Delete a range of content from a Google Doc |
| [Google Docs Export](block-integrations/google/docs.md#google-docs-export) | Export a Google Doc to PDF, Word, text, or other formats |
| [Google Docs Find Replace Plain Text](block-integrations/google/docs.md#google-docs-find-replace-plain-text) | Find and replace plain text in a Google Doc (no formatting applied to replacement) |
| [Google Docs Format Text](block-integrations/google/docs.md#google-docs-format-text) | Apply formatting (bold, italic, color, etc |
| [Google Docs Get Metadata](block-integrations/google/docs.md#google-docs-get-metadata) | Get metadata about a Google Doc |
| [Google Docs Get Structure](block-integrations/google/docs.md#google-docs-get-structure) | Get document structure with index positions for precise editing operations |
| [Google Docs Insert Markdown At](block-integrations/google/docs.md#google-docs-insert-markdown-at) | Insert formatted Markdown at a specific position in a Google Doc - ideal for LLM/AI output |
| [Google Docs Insert Page Break](block-integrations/google/docs.md#google-docs-insert-page-break) | Insert a page break into a Google Doc |
| [Google Docs Insert Plain Text](block-integrations/google/docs.md#google-docs-insert-plain-text) | Insert plain text at a specific position in a Google Doc (no formatting applied) |
| [Google Docs Insert Table](block-integrations/google/docs.md#google-docs-insert-table) | Insert a table into a Google Doc, optionally with content and Markdown formatting |
| [Google Docs Read](block-integrations/google/docs.md#google-docs-read) | Read text content from a Google Doc |
| [Google Docs Replace All With Markdown](block-integrations/google/docs.md#google-docs-replace-all-with-markdown) | Replace entire Google Doc content with formatted Markdown - ideal for LLM/AI output |
| [Google Docs Replace Content With Markdown](block-integrations/google/docs.md#google-docs-replace-content-with-markdown) | Find text and replace it with formatted Markdown - ideal for LLM/AI output and templates |
| [Google Docs Replace Range With Markdown](block-integrations/google/docs.md#google-docs-replace-range-with-markdown) | Replace a specific index range in a Google Doc with formatted Markdown - ideal for LLM/AI output |
| [Google Docs Set Public Access](block-integrations/google/docs.md#google-docs-set-public-access) | Make a Google Doc public or private |
| [Google Docs Share](block-integrations/google/docs.md#google-docs-share) | Share a Google Doc with specific users |
| [Google Sheets Add Column](block-integrations/google/sheets.md#google-sheets-add-column) | Add a new column with a header |
| [Google Sheets Add Dropdown](block-integrations/google/sheets.md#google-sheets-add-dropdown) | Add a dropdown list (data validation) to cells |
| [Google Sheets Add Note](block-integrations/google/sheets.md#google-sheets-add-note) | Add a note to a cell in a Google Sheet |
| [Google Sheets Append Row](block-integrations/google/sheets.md#google-sheets-append-row) | Append or Add a single row to the end of a Google Sheet |
| [Google Sheets Batch Operations](block-integrations/google/sheets.md#google-sheets-batch-operations) | This block performs multiple operations on a Google Sheets spreadsheet in a single batch request |
| [Google Sheets Clear](block-integrations/google/sheets.md#google-sheets-clear) | This block clears data from a specified range in a Google Sheets spreadsheet |
| [Google Sheets Copy To Spreadsheet](block-integrations/google/sheets.md#google-sheets-copy-to-spreadsheet) | Copy a sheet from one spreadsheet to another |
| [Google Sheets Create Named Range](block-integrations/google/sheets.md#google-sheets-create-named-range) | Create a named range to reference cells by name instead of A1 notation |
| [Google Sheets Create Spreadsheet](block-integrations/google/sheets.md#google-sheets-create-spreadsheet) | This block creates a new Google Sheets spreadsheet with specified sheets |
| [Google Sheets Delete Column](block-integrations/google/sheets.md#google-sheets-delete-column) | Delete a column by header name or column letter |
| [Google Sheets Delete Rows](block-integrations/google/sheets.md#google-sheets-delete-rows) | Delete specific rows from a Google Sheet by their row indices |
| [Google Sheets Export Csv](block-integrations/google/sheets.md#google-sheets-export-csv) | Export a Google Sheet as CSV data |
| [Google Sheets Filter Rows](block-integrations/google/sheets.md#google-sheets-filter-rows) | Filter rows in a Google Sheet based on a column condition |
| [Google Sheets Find](block-integrations/google/sheets.md#google-sheets-find) | Find text in a Google Sheets spreadsheet |
| [Google Sheets Find Replace](block-integrations/google/sheets.md#google-sheets-find-replace) | This block finds and replaces text in a Google Sheets spreadsheet |
| [Google Sheets Format](block-integrations/google/sheets.md#google-sheets-format) | Format a range in a Google Sheet (sheet optional) |
| [Google Sheets Get Column](block-integrations/google/sheets.md#google-sheets-get-column) | Extract all values from a specific column |
| [Google Sheets Get Notes](block-integrations/google/sheets.md#google-sheets-get-notes) | Get notes from cells in a Google Sheet |
| [Google Sheets Get Row](block-integrations/google/sheets.md#google-sheets-get-row) | Get a specific row by its index |
| [Google Sheets Get Row Count](block-integrations/google/sheets.md#google-sheets-get-row-count) | Get row count and dimensions of a Google Sheet |
| [Google Sheets Get Unique Values](block-integrations/google/sheets.md#google-sheets-get-unique-values) | Get unique values from a column |
| [Google Sheets Import Csv](block-integrations/google/sheets.md#google-sheets-import-csv) | Import CSV data into a Google Sheet |
| [Google Sheets Insert Row](block-integrations/google/sheets.md#google-sheets-insert-row) | Insert a single row at a specific position |
| [Google Sheets List Named Ranges](block-integrations/google/sheets.md#google-sheets-list-named-ranges) | List all named ranges in a spreadsheet |
| [Google Sheets Lookup Row](block-integrations/google/sheets.md#google-sheets-lookup-row) | Look up a row by finding a value in a specific column |
| [Google Sheets Manage Sheet](block-integrations/google/sheets.md#google-sheets-manage-sheet) | Create, delete, or copy sheets (sheet optional) |
| [Google Sheets Metadata](block-integrations/google/sheets.md#google-sheets-metadata) | This block retrieves metadata about a Google Sheets spreadsheet including sheet names and properties |
| [Google Sheets Protect Range](block-integrations/google/sheets.md#google-sheets-protect-range) | Protect a cell range or entire sheet from editing |
| [Google Sheets Read](block-integrations/google/sheets.md#google-sheets-read) | A block that reads data from a Google Sheets spreadsheet using A1 notation range selection |
| [Google Sheets Remove Duplicates](block-integrations/google/sheets.md#google-sheets-remove-duplicates) | Remove duplicate rows based on specified columns |
| [Google Sheets Set Public Access](block-integrations/google/sheets.md#google-sheets-set-public-access) | Make a Google Spreadsheet public or private |
| [Google Sheets Share Spreadsheet](block-integrations/google/sheets.md#google-sheets-share-spreadsheet) | Share a Google Spreadsheet with users or get shareable link |
| [Google Sheets Sort](block-integrations/google/sheets.md#google-sheets-sort) | Sort a Google Sheet by one or two columns |
| [Google Sheets Update Cell](block-integrations/google/sheets.md#google-sheets-update-cell) | Update a single cell in a Google Sheets spreadsheet |
| [Google Sheets Update Row](block-integrations/google/sheets.md#google-sheets-update-row) | Update a specific row by its index |
| [Google Sheets Write](block-integrations/google/sheets.md#google-sheets-write) | A block that writes data to a Google Sheets spreadsheet at a specified A1 notation range |
| [JSON Decoder](block-integrations/data.md#json-decoder) | Decodes a JSON string into the value or data structure, it represents, e |
| [JSON Encoder](block-integrations/data.md#json-encoder) | Encodes any value or data structure into a JSON string |
| [Keyword Suggestion Extractor](block-integrations/dataforseo/keyword_suggestions.md#keyword-suggestion-extractor) | Extract individual fields from a KeywordSuggestion object |
| [Persist Information](block-integrations/data.md#persist-information) | Persists a key-value pair for use across multiple runs of an agent |
| [RMFG Analyze Design](block-integrations/rmfg/designs.md#rmfg-analyze-design) | Uploads a STEP file to RMFG, a manufacturer that makes and ships real sheet-metal and tube-laser parts, and returns its analyzed parts: the first step before a live quote, cart or order |
| [RMFG Create Cart](block-integrations/rmfg/carts.md#rmfg-create-cart) | Creates an RMFG cart with a website checkout link for one or more configured designs, priced live with shipping and tax once an address is set |
| [RMFG Create DFM Report](block-integrations/rmfg/dfm.md#rmfg-create-dfm-report) | Runs an RMFG manufacturability (DFM) check on a configured design |
| [RMFG Create Quote](block-integrations/rmfg/quotes.md#rmfg-create-quote) | Gets a live price from RMFG, a manufacturer that laser-cuts, bends and ships real sheet-metal and tube parts, plus manufacturability findings |
| [RMFG Create Review Link](block-integrations/rmfg/review_links.md#rmfg-create-review-link) | Creates an RMFG review link so a person can inspect and adjust a design |
| [RMFG Get Cart](block-integrations/rmfg/carts.md#rmfg-get-cart) | Fetches an RMFG cart and its latest quote by ID |
| [RMFG Get DFM Report](block-integrations/rmfg/dfm.md#rmfg-get-dfm-report) | Fetches an RMFG DFM report by ID |
| [RMFG Get Design](block-integrations/rmfg/designs.md#rmfg-get-design) | Fetches an RMFG design and its analyzed parts by ID |
| [RMFG Get Order](block-integrations/rmfg/orders.md#rmfg-get-order) | Fetches an RMFG order's status and shipment tracking |
| [RMFG Get Quote](block-integrations/rmfg/quotes.md#rmfg-get-quote) | Fetches an RMFG quote by ID |
| [RMFG Get Review Link](block-integrations/rmfg/review_links.md#rmfg-get-review-link) | Fetches an RMFG review link and the configuration a person saved on it |
| [RMFG List Finishes](block-integrations/rmfg/catalog.md#rmfg-list-finishes) | Lists the finishes RMFG can apply to sheet or tube parts |
| [RMFG List Hardware](block-integrations/rmfg/catalog.md#rmfg-list-hardware) | Lists the taps, studs, nuts or standoffs RMFG can install |
| [RMFG List Materials](block-integrations/rmfg/catalog.md#rmfg-list-materials) | Lists the sheet-metal stock RMFG can cut and bend, with thickness in mm and inches |
| [RMFG List Orders](block-integrations/rmfg/orders.md#rmfg-list-orders) | Lists the RMFG account's manufacturing orders |
| [RMFG List Powder Coat Colors](block-integrations/rmfg/catalog.md#rmfg-list-powder-coat-colors) | Lists the powder-coat colors RMFG offers |
| [RMFG List Tube Profiles](block-integrations/rmfg/catalog.md#rmfg-list-tube-profiles) | Lists the tube stock profiles RMFG can laser-cut |
| [RMFG Pay Cart](block-integrations/rmfg/pay_cart.md#rmfg-pay-cart) | Pays an RMFG cart with the saved card and places a real production order |
| [RMFG Update Cart](block-integrations/rmfg/carts.md#rmfg-update-cart) | Updates an open RMFG cart's address, shipping option or items |
| [Read Spreadsheet](block-integrations/data.md#read-spreadsheet) | Reads CSV and Excel files and outputs the data as a list of dictionaries and individual rows |
| [Related Keyword Extractor](block-integrations/dataforseo/related_keywords.md#related-keyword-extractor) | Extract individual fields from a RelatedKeyword object |
| [Retrieve Information](block-integrations/data.md#retrieve-information) | Reads back a key-value pair previously saved by PersistInformationBlock |
| [SQL Query](block-integrations/data.md#sql-query) | Execute a SQL query |
| [Screenshot Web Page](block-integrations/data.md#screenshot-web-page) | Takes a screenshot of a specified website using ScreenshotOne API |
| [Stripe Link Create Card Spend Request](block-integrations/stripe_link/spend_request.md#stripe-link-create-card-spend-request) | Create a Stripe Link spend request for a one-time virtual card |
| [Stripe Link Create Token Spend Request](block-integrations/stripe_link/spend_request.md#stripe-link-create-token-spend-request) | MPP step 2 of 3: ask the user to authorize a payment to a merchant that answers HTTP 402, and provision a Shared Payment Token for it |
| [Stripe Link Get Payment Challenge](block-integrations/stripe_link/mpp.md#stripe-link-get-payment-challenge) | MPP step 1 of 3: read a merchant's HTTP 402 payment challenge to learn its network ID and amount |
| [Stripe Link Get Shipping Address](block-integrations/stripe_link/profile.md#stripe-link-get-shipping-address) | Get the delivery addresses saved on the user's Link wallet, with the default one resolved for you |
| [Stripe Link Get Spend Request Status](block-integrations/stripe_link/spend_request.md#stripe-link-get-spend-request-status) | Check whether a Stripe Link spend request has been approved yet |
| [Stripe Link Get User Info](block-integrations/stripe_link/profile.md#stripe-link-get-user-info) | Get the Link account holder's name, email and phone |
| [Stripe Link List Payment Methods](block-integrations/stripe_link/spend_request.md#stripe-link-list-payment-methods) | List the cards and bank accounts in the user's Link wallet |
| [Stripe Link MPP Pay](block-integrations/stripe_link/mpp.md#stripe-link-mpp-pay) | MPP step 3 of 3: spend an approved Shared Payment Token at the merchant's endpoint |
| [Stripe Link Retrieve Card](block-integrations/stripe_link/spend_request.md#stripe-link-retrieve-card) | Get the one-time virtual card number and CVC for an approved spend request, to type into a normal checkout form |
| [Stripe Subscription Trigger](block-integrations/stripe/triggers.md#stripe-subscription-trigger) | Triggers on Stripe subscription events (new, upgrade, cancel) |

## Text Processing

| Block Name | Description |
|------------|-------------|
| [Code Extraction](block-integrations/text.md#code-extraction) | Extracts code blocks from text and identifies their programming languages |
| [Combine Texts](block-integrations/text.md#combine-texts) | This block combines multiple input texts into a single output text |
| [Countdown Timer](block-integrations/text.md#countdown-timer) | This block triggers after a specified duration |
| [Extract Text Information](block-integrations/text.md#extract-text-information) | This block extracts the text from the given text using the pattern (regex) |
| [Fill Text Template](block-integrations/text.md#fill-text-template) | This block formats the given texts using the format template |
| [Get Current Date](block-integrations/text.md#get-current-date) | This block outputs the current date with an optional offset |
| [Get Current Date And Time](block-integrations/text.md#get-current-date-and-time) | This block outputs the current date and time |
| [Get Current Time](block-integrations/text.md#get-current-time) | This block outputs the current time |
| [Match Text Pattern](block-integrations/text.md#match-text-pattern) | Matches text against a regex pattern and forwards data to positive or negative output based on the match |
| [Text Decoder](block-integrations/text.md#text-decoder) | Decodes a string containing escape sequences into actual text |
| [Text Encoder](block-integrations/text.md#text-encoder) | Encodes a string by converting special characters into escape sequences |
| [Text Replace](block-integrations/text.md#text-replace) | This block is used to replace a text with a new text |
| [Text Split](block-integrations/text.md#text-split) | This block is used to split a text into a list of strings |
| [Word Character Count](block-integrations/text.md#word-character-count) | Counts the number of words and characters in a given text |

## AI and Language Models

| Block Name | Description |
|------------|-------------|
| [AI Ad Maker Video Creator](block-integrations/llm.md#ai-ad-maker-video-creator) | Creates an AI‑generated 30‑second advert (text + images) |
| [AI Condition](block-integrations/llm.md#ai-condition) | Uses AI to evaluate natural language conditions and provide conditional outputs |
| [AI Conversation](block-integrations/llm.md#ai-conversation) | A block that facilitates multi-turn conversations with a Large Language Model (LLM), maintaining context across message exchanges |
| [AI Image Customizer](block-integrations/llm.md#ai-image-customizer) | Generate and edit custom images using Google's Nano-Banana models from Gemini |
| [AI Image Editor](block-integrations/llm.md#ai-image-editor) | Edit images using Flux Kontext or Google Nano Banana models |
| [AI Image Generator](block-integrations/llm.md#ai-image-generator) | Generate images using various AI models through a unified interface |
| [AI List Generator](block-integrations/llm.md#ai-list-generator) | A block that creates lists of items based on prompts using a Large Language Model (LLM), with optional source data for context |
| [AI Music Generator](block-integrations/llm.md#ai-music-generator) | This block generates music using Meta's MusicGen model on Replicate |
| [AI Screenshot To Video Ad](block-integrations/llm.md#ai-screenshot-to-video-ad) | Turns a screenshot into an engaging, avatar‑narrated video advert |
| [AI Shortform Video Creator](block-integrations/llm.md#ai-shortform-video-creator) | Creates a shortform video using revid |
| [AI Structured Response Generator](block-integrations/llm.md#ai-structured-response-generator) | A block that generates structured JSON responses using a Large Language Model (LLM), with schema validation and format enforcement |
| [AI Text Generator](block-integrations/llm.md#ai-text-generator) | A block that produces text responses using a Large Language Model (LLM) based on customizable prompts and system instructions |
| [AI Text Summarizer](block-integrations/llm.md#ai-text-summarizer) | A block that summarizes long texts using a Large Language Model (LLM), with configurable focus topics and summary styles |
| [AI Video Generator](block-integrations/fal/ai_video_generator.md#ai-video-generator) | Generate videos using FAL AI models |
| [Bannerbear Text Overlay](block-integrations/bannerbear/text_overlay.md#bannerbear-text-overlay) | Add text overlay to images using Bannerbear templates |
| [Claude Code](block-integrations/llm.md#claude-code) | Execute tasks using Claude Code in an E2B sandbox |
| [Code Generation](block-integrations/llm.md#code-generation) | Generate or refactor code using an OpenAI API key or a connected ChatGPT plan through Codex App Server |
| [Create Talking Avatar Video](block-integrations/llm.md#create-talking-avatar-video) | This block integrates with D-ID to create video clips and retrieve their URLs |
| [Exa Answer](block-integrations/exa/answers.md#exa-answer) | Get an LLM answer to a question informed by Exa search results |
| [Exa Create Enrichment](block-integrations/exa/websets_enrichment.md#exa-create-enrichment) | Create enrichments to extract additional structured data from webset items |
| [Exa Create Research](block-integrations/exa/research.md#exa-create-research) | Create research task with optional waiting - explores web and synthesizes findings with citations |
| [Ideogram Model](block-integrations/llm.md#ideogram-model) | This block runs Ideogram models with both simple and advanced settings |
| [Jev Ask Many](block-integrations/typesafe/ask_many.md#jev-ask-many) | Ask multiple Choice, Score, or Noul questions of one shared state with Jev in a single call |
| [Jev Choice](block-integrations/typesafe/choice.md#jev-choice) | Make a typed choice with Jev |
| [Jev Filter](block-integrations/typesafe/filter.md#jev-filter) | Filter items with sequential Jev scores, one call per item |
| [Jev Pick Best](block-integrations/typesafe/pick_best.md#jev-pick-best) | Choose the best candidate using Jev and rank candidates directly by its probabilities |
| [Jev Route](block-integrations/typesafe/route.md#jev-route) | Route data using Jev's typed choice |
| [Jev Score](block-integrations/typesafe/score.md#jev-score) | Score evidence with Jev using an explicit ordered scale |
| [Jev Yes No](block-integrations/typesafe/yes_no.md#jev-yes-no) | Ask Jev a plain-language yes/no question and forward data to the chosen pin, or unsure below your confidence threshold |
| [Jina Chunking](block-integrations/jina/chunking.md#jina-chunking) | Chunks texts using Jina AI's segmentation service |
| [Jina Embedding](block-integrations/jina/embeddings.md#jina-embedding) | Generates embeddings using Jina AI |
| [Orchestrator](block-integrations/llm.md#orchestrator) | Uses AI to intelligently decide what tool to use |
| [Perplexity](block-integrations/llm.md#perplexity) | Query Perplexity's sonar models with real-time web search capabilities and receive annotated responses with source citations |
| [Replicate Flux Advanced Model](block-integrations/replicate/flux_advanced.md#replicate-flux-advanced-model) | This block runs Flux models on Replicate with advanced settings |
| [Replicate Model](block-integrations/replicate/replicate_block.md#replicate-model) | Run Replicate models synchronously |
| [Smart Search](block-integrations/dataforb2b/reasoning.md#smart-search) | Natural-language search for people, leads or companies using DataForB2B's B2B database — describe your ideal lead or ICP in plain English and get matching profiles |
| [Stagehand Act](block-integrations/stagehand/blocks.md#stagehand-act) | Interact with a web page by performing actions on a web page |
| [Stagehand Extract](block-integrations/stagehand/blocks.md#stagehand-extract) | Extract structured data from a webpage |
| [Stagehand Observe](block-integrations/stagehand/blocks.md#stagehand-observe) | Find suggested actions for your workflows |
| [Unreal Text To Speech](block-integrations/llm.md#unreal-text-to-speech) | Converts text to speech using the Unreal Speech API |
| [Video Narration](block-integrations/video/narration.md#video-narration) | Generate AI narration and add to video |

## Search and Information Retrieval

| Block Name | Description |
|------------|-------------|
| [AnySearch](block-integrations/anysearch/search.md#anysearch) | Searches the web using AnySearch - general queries plus vertical domains (finance, academic, health, legal, and more) via domain/sub_domain filters |
| [AnySearch Extract](block-integrations/anysearch/extract.md#anysearch-extract) | Extracts readable content from a single URL using AnySearch, optimized for LLM consumption |
| [AnySearch Parallel Search](block-integrations/anysearch/parallel_search.md#anysearch-parallel-search) | Runs several AnySearch queries in parallel (client-side concurrency via asyncio) |
| [Ask Wolfram](block-integrations/wolfram/llm_api.md#ask-wolfram) | Ask Wolfram Alpha a question |
| [Exa Bulk Webset Items](block-integrations/exa/websets_items.md#exa-bulk-webset-items) | Get all items from a webset in bulk (with configurable limits) |
| [Exa Cancel Enrichment](block-integrations/exa/websets_enrichment.md#exa-cancel-enrichment) | Cancel a running enrichment operation |
| [Exa Cancel Webset](block-integrations/exa/websets.md#exa-cancel-webset) | Cancel all operations being performed on a Webset |
| [Exa Cancel Webset Search](block-integrations/exa/websets_search.md#exa-cancel-webset-search) | Cancel a running webset search |
| [Exa Contents](block-integrations/exa/contents.md#exa-contents) | Retrieves document contents using Exa's contents API |
| [Exa Create Monitor](block-integrations/exa/websets_monitor.md#exa-create-monitor) | Create automated monitors to keep websets updated with fresh data on a schedule |
| [Exa Create Or Find Webset](block-integrations/exa/websets.md#exa-create-or-find-webset) | Create a new webset or return existing one by external_id (idempotent operation) |
| [Exa Create Webset](block-integrations/exa/websets.md#exa-create-webset) | Create a new Exa Webset for persistent web search collections with optional waiting for initial results |
| [Exa Create Webset Search](block-integrations/exa/websets_search.md#exa-create-webset-search) | Add a new search to an existing webset to find more items |
| [Exa Delete Enrichment](block-integrations/exa/websets_enrichment.md#exa-delete-enrichment) | Delete an enrichment from a webset |
| [Exa Delete Monitor](block-integrations/exa/websets_monitor.md#exa-delete-monitor) | Delete a monitor from a webset |
| [Exa Delete Webset](block-integrations/exa/websets.md#exa-delete-webset) | Delete a Webset and all its items |
| [Exa Delete Webset Item](block-integrations/exa/websets_items.md#exa-delete-webset-item) | Delete a specific item from a webset |
| [Exa Find Or Create Search](block-integrations/exa/websets_search.md#exa-find-or-create-search) | Find existing search by query or create new - prevents duplicate searches in workflows |
| [Exa Find Similar](block-integrations/exa/similar.md#exa-find-similar) | Finds similar links using Exa's findSimilar API |
| [Exa Get Enrichment](block-integrations/exa/websets_enrichment.md#exa-get-enrichment) | Get the status and details of a webset enrichment |
| [Exa Get Monitor](block-integrations/exa/websets_monitor.md#exa-get-monitor) | Get the details and status of a webset monitor |
| [Exa Get Research](block-integrations/exa/research.md#exa-get-research) | Get status and results of a research task |
| [Exa Get Webset](block-integrations/exa/websets.md#exa-get-webset) | Retrieve a Webset by ID or external ID |
| [Exa Get Webset Item](block-integrations/exa/websets_items.md#exa-get-webset-item) | Get a specific item from a webset by its ID |
| [Exa Get Webset Search](block-integrations/exa/websets_search.md#exa-get-webset-search) | Get the status and details of a webset search |
| [Exa List Monitors](block-integrations/exa/websets_monitor.md#exa-list-monitors) | List all monitors with optional webset filtering |
| [Exa List Research](block-integrations/exa/research.md#exa-list-research) | List all research tasks with pagination support |
| [Exa List Webset Items](block-integrations/exa/websets_items.md#exa-list-webset-items) | List items in a webset with pagination support |
| [Exa List Websets](block-integrations/exa/websets.md#exa-list-websets) | List all Websets with pagination support |
| [Exa Preview Webset](block-integrations/exa/websets.md#exa-preview-webset) | Preview how a search query will be interpreted before creating a webset |
| [Exa Search](block-integrations/exa/search.md#exa-search) | Searches the web using Exa's advanced search API |
| [Exa Update Enrichment](block-integrations/exa/websets_enrichment.md#exa-update-enrichment) | Update an existing enrichment configuration |
| [Exa Update Monitor](block-integrations/exa/websets_monitor.md#exa-update-monitor) | Update a monitor's status, schedule, or metadata |
| [Exa Update Webset](block-integrations/exa/websets.md#exa-update-webset) | Update metadata for an existing Webset |
| [Exa Wait For Enrichment](block-integrations/exa/websets_polling.md#exa-wait-for-enrichment) | Wait for a webset enrichment to complete with progress tracking |
| [Exa Wait For Research](block-integrations/exa/research.md#exa-wait-for-research) | Wait for a research task to complete with configurable timeout |
| [Exa Wait For Search](block-integrations/exa/websets_polling.md#exa-wait-for-search) | Wait for a specific webset search to complete with progress tracking |
| [Exa Wait For Webset](block-integrations/exa/websets_polling.md#exa-wait-for-webset) | Wait for a webset to reach a specific status with progress tracking |
| [Exa Webset Items Summary](block-integrations/exa/websets_items.md#exa-webset-items-summary) | Get a summary of webset items without retrieving all data |
| [Exa Webset Status](block-integrations/exa/websets.md#exa-webset-status) | Get a quick status overview of a webset |
| [Exa Webset Summary](block-integrations/exa/websets.md#exa-webset-summary) | Get a comprehensive summary of a webset with samples and statistics |
| [Extract Website Content](block-integrations/jina/search.md#extract-website-content) | This block scrapes the content from the given web URL |
| [Fact Checker](block-integrations/jina/fact_checker.md#fact-checker) | This block checks the factuality of a given statement using Jina AI's Grounding API |
| [Firecrawl Crawl](block-integrations/firecrawl/crawl.md#firecrawl-crawl) | Firecrawl crawls websites to extract comprehensive data while bypassing blockers |
| [Firecrawl Extract](block-integrations/firecrawl/extract.md#firecrawl-extract) | Firecrawl crawls websites to extract comprehensive data while bypassing blockers |
| [Firecrawl Map Website](block-integrations/firecrawl/map.md#firecrawl-map-website) | Firecrawl maps a website to extract all the links |
| [Firecrawl Scrape](block-integrations/firecrawl/scrape.md#firecrawl-scrape) | Firecrawl scrapes a website to extract comprehensive data while bypassing blockers |
| [Firecrawl Search](block-integrations/firecrawl/search.md#firecrawl-search) | Firecrawl searches the web for the given query |
| [Get Person Detail](block-integrations/apollo/person.md#get-person-detail) | Get detailed person data with Apollo API, including email reveal |
| [Get Wikipedia Summary](block-integrations/search.md#get-wikipedia-summary) | This block fetches the summary of a given topic from Wikipedia |
| [Google Maps Search](block-integrations/search.md#google-maps-search) | This block searches for local businesses using Google Maps API |
| [Hacker News Get Item](block-integrations/hacker_news/items.md#hacker-news-get-item) | Get a Hacker News story, comment, poll or job by its id or link, with its comments as a list in reading order |
| [Hacker News Get Stories](block-integrations/hacker_news/stories.md#hacker-news-get-stories) | Get the top (front page), new, best, Ask HN, Show HN or job stories from Hacker News, in the order Hacker News ranks them |
| [Hacker News Get User](block-integrations/hacker_news/users.md#hacker-news-get-user) | Get a Hacker News user's profile by username: karma, when the account was created, the about text and how many items they have posted |
| [Hacker News Search](block-integrations/hacker_news/search.md#hacker-news-search) | Search Hacker News stories and comments by keyword, author, date, points or linked domain, sorted by relevance or newest first |
| [Search Filter Typeahead](block-integrations/dataforb2b/typeahead.md#search-filter-typeahead) | Resolve the exact filter value (company, industry, job title, skill, school, investor, location, category) for people and company searches with DataForB2B |
| [Search Organizations](block-integrations/apollo/organization.md#search-organizations) | Search for organizations in Apollo |
| [Search People](block-integrations/apollo/people.md#search-people) | Search for people in Apollo |
| [Search The Web](block-integrations/jina/search.md#search-the-web) | This block searches the internet for the given search query |
| [Tavily Crawl](block-integrations/tavily/crawl.md#tavily-crawl) | Crawls a website with Tavily, following links from the root URL and extracting page content |
| [Tavily Extract](block-integrations/tavily/extract.md#tavily-extract) | Extracts page content from one or more URLs using Tavily, optimized for LLM consumption |
| [Tavily Map](block-integrations/tavily/map.md#tavily-map) | Maps a website's structure with Tavily, discovering its URLs without extracting content |
| [Tavily Search](block-integrations/tavily/search.md#tavily-search) | Searches the web using Tavily's AI-native search API |
| [Validate Emails](block-integrations/zerobounce/validate_emails.md#validate-emails) | Validate emails |

## Social Media and Content

| Block Name | Description |
|------------|-------------|
| [Approve Reddit Post](block-integrations/misc.md#approve-reddit-post) | Approves a Reddit post or comment from the mod queue |
| [Ban Subreddit User](block-integrations/misc.md#ban-subreddit-user) | Bans a user from a subreddit |
| [Create Discord Thread](block-integrations/discord/bot_blocks.md#create-discord-thread) | Creates a new thread in a Discord channel |
| [Create Reddit Post](block-integrations/misc.md#create-reddit-post) | Create a new post on a subreddit |
| [Delete Reddit Comment](block-integrations/misc.md#delete-reddit-comment) | Delete a Reddit comment that you own |
| [Delete Reddit Post](block-integrations/misc.md#delete-reddit-post) | Delete a Reddit post that you own |
| [Delete Telegram Message](block-integrations/telegram/blocks.md#delete-telegram-message) | Delete a message from a Telegram chat |
| [Discord Channel Info](block-integrations/discord/bot_blocks.md#discord-channel-info) | Resolves Discord channel names to IDs and vice versa |
| [Discord Get Current User](block-integrations/discord/oauth_blocks.md#discord-get-current-user) | Gets information about the currently authenticated Discord user using OAuth2 credentials |
| [Discord User Info](block-integrations/discord/bot_blocks.md#discord-user-info) | Gets information about a Discord user by their ID |
| [Edit Reddit Post](block-integrations/misc.md#edit-reddit-post) | Edit the body text of an existing Reddit post that you own |
| [Edit Telegram Message](block-integrations/telegram/blocks.md#edit-telegram-message) | Edit the text of an existing message sent by the bot |
| [Get Linkedin Profile](block-integrations/enrichlayer/linkedin.md#get-linkedin-profile) | Fetch LinkedIn profile data using Enrichlayer |
| [Get Linkedin Profile Picture](block-integrations/enrichlayer/linkedin.md#get-linkedin-profile-picture) | Get LinkedIn profile pictures using Enrichlayer |
| [Get Reddit Comment](block-integrations/misc.md#get-reddit-comment) | Get details about a specific Reddit comment by its ID |
| [Get Reddit Comment Replies](block-integrations/misc.md#get-reddit-comment-replies) | Get replies to a specific Reddit comment |
| [Get Reddit Inbox](block-integrations/misc.md#get-reddit-inbox) | Get messages, mentions, and comment replies from your Reddit inbox |
| [Get Reddit Post](block-integrations/misc.md#get-reddit-post) | Get detailed information about a specific Reddit post by its ID |
| [Get Reddit Post Comments](block-integrations/misc.md#get-reddit-post-comments) | Get top-level comments on a Reddit post |
| [Get Reddit Posts](block-integrations/misc.md#get-reddit-posts) | This block fetches Reddit posts from a defined subreddit name |
| [Get Reddit User Info](block-integrations/misc.md#get-reddit-user-info) | Get information about a Reddit user including karma, account age, and verification status |
| [Get Subreddit Flairs](block-integrations/misc.md#get-subreddit-flairs) | Get available link flair options for a subreddit |
| [Get Subreddit Info](block-integrations/misc.md#get-subreddit-info) | Get information about a subreddit including subscriber count, description, and rules |
| [Get Subreddit Rules](block-integrations/misc.md#get-subreddit-rules) | Get the rules for a subreddit to ensure compliance before posting |
| [Get Telegram File](block-integrations/telegram/blocks.md#get-telegram-file) | Download a file from Telegram using its file_id |
| [Get User Posts](block-integrations/misc.md#get-user-posts) | Fetch posts by a specific Reddit user |
| [Linkedin Person Lookup](block-integrations/enrichlayer/linkedin.md#linkedin-person-lookup) | Look up LinkedIn profiles by person information using Enrichlayer |
| [Linkedin Role Lookup](block-integrations/enrichlayer/linkedin.md#linkedin-role-lookup) | Look up LinkedIn profiles by role in a company using Enrichlayer |
| [Lock Reddit Post](block-integrations/misc.md#lock-reddit-post) | Locks or unlocks a Reddit post or comment to prevent or allow replies |
| [Mod Queue](block-integrations/misc.md#mod-queue) | Fetches the mod queue for a subreddit |
| [Post Reddit Comment](block-integrations/misc.md#post-reddit-comment) | This block posts a Reddit comment on a specified Reddit post |
| [Post To Bluesky](block-integrations/ayrshare/post_to_bluesky.md#post-to-bluesky) | Post to Bluesky using Ayrshare |
| [Post To Facebook](block-integrations/ayrshare/post_to_facebook.md#post-to-facebook) | Post to Facebook using Ayrshare |
| [Post To GMB](block-integrations/ayrshare/post_to_gmb.md#post-to-gmb) | Post to Google My Business using Ayrshare |
| [Post To Instagram](block-integrations/ayrshare/post_to_instagram.md#post-to-instagram) | Post to Instagram using Ayrshare |
| [Post To LinkedIn](block-integrations/ayrshare/post_to_linkedin.md#post-to-linkedin) | Post to LinkedIn using Ayrshare |
| [Post To Pinterest](block-integrations/ayrshare/post_to_pinterest.md#post-to-pinterest) | Post to Pinterest using Ayrshare |
| [Post To Reddit](block-integrations/ayrshare/post_to_reddit.md#post-to-reddit) | Post to Reddit using Ayrshare |
| [Post To Snapchat](block-integrations/ayrshare/post_to_snapchat.md#post-to-snapchat) | Post to Snapchat using Ayrshare |
| [Post To Telegram](block-integrations/ayrshare/post_to_telegram.md#post-to-telegram) | Post to Telegram using Ayrshare |
| [Post To Threads](block-integrations/ayrshare/post_to_threads.md#post-to-threads) | Post to Threads using Ayrshare |
| [Post To TikTok](block-integrations/ayrshare/post_to_tiktok.md#post-to-tiktok) | Post to TikTok using Ayrshare |
| [Post To X](block-integrations/ayrshare/post_to_x.md#post-to-x) | Post to X / Twitter using Ayrshare |
| [Post To YouTube](block-integrations/ayrshare/post_to_youtube.md#post-to-youtube) | Post to YouTube using Ayrshare |
| [Publish To Medium](block-integrations/misc.md#publish-to-medium) | Publishes a post to Medium |
| [Read Discord Messages](block-integrations/discord/bot_blocks.md#read-discord-messages) | Reads new messages from a Discord channel using a bot token and triggers when a new message is posted |
| [Reddit Get My Posts](block-integrations/misc.md#reddit-get-my-posts) | Fetch posts created by the authenticated Reddit user (you) |
| [Remove Reddit Post](block-integrations/misc.md#remove-reddit-post) | Removes a Reddit post or comment as a moderator |
| [Reply To Discord Message](block-integrations/discord/bot_blocks.md#reply-to-discord-message) | Replies to a specific Discord message |
| [Reply To Reddit Comment](block-integrations/misc.md#reply-to-reddit-comment) | Reply to a specific Reddit comment |
| [Reply To Telegram Message](block-integrations/telegram/blocks.md#reply-to-telegram-message) | Reply to a specific message in a Telegram chat |
| [Search Reddit](block-integrations/misc.md#search-reddit) | Search Reddit for posts matching a query |
| [Send Discord DM](block-integrations/discord/bot_blocks.md#send-discord-dm) | Sends a direct message to a Discord user using their user ID |
| [Send Discord Embed](block-integrations/discord/bot_blocks.md#send-discord-embed) | Sends a rich embed message to a Discord channel |
| [Send Discord File](block-integrations/discord/bot_blocks.md#send-discord-file) | Sends a file attachment to a Discord channel |
| [Send Discord Message](block-integrations/discord/bot_blocks.md#send-discord-message) | Sends a message to a Discord channel using a bot token |
| [Send Mod Mail](block-integrations/misc.md#send-mod-mail) | Sends a modmail message from a subreddit to a user |
| [Send Reddit Message](block-integrations/misc.md#send-reddit-message) | Send a private message (DM) to a Reddit user |
| [Send Slack Message](block-integrations/slack/blocks.md#send-slack-message) | Send a text message to any Slack channel, DM, or thread |
| [Send Telegram Audio](block-integrations/telegram/blocks.md#send-telegram-audio) | Send an audio file to a Telegram chat |
| [Send Telegram Document](block-integrations/telegram/blocks.md#send-telegram-document) | Send a document (any file type) to a Telegram chat |
| [Send Telegram Message](block-integrations/telegram/blocks.md#send-telegram-message) | Send a text message to a Telegram chat |
| [Send Telegram Photo](block-integrations/telegram/blocks.md#send-telegram-photo) | Send a photo to a Telegram chat |
| [Send Telegram Video](block-integrations/telegram/blocks.md#send-telegram-video) | Send a video to a Telegram chat |
| [Send Telegram Voice](block-integrations/telegram/blocks.md#send-telegram-voice) | Send a voice message to a Telegram chat |
| [Telegram Message Reaction Trigger](block-integrations/telegram/triggers.md#telegram-message-reaction-trigger) | Triggers when a reaction to a message is changed |
| [Telegram Message Trigger](block-integrations/telegram/triggers.md#telegram-message-trigger) | Triggers when a message is received or edited in your Telegram bot |
| [Transcribe Youtube Video](block-integrations/misc.md#transcribe-youtube-video) | Transcribes a YouTube video using a proxy |
| [Twitter Add List Member](block-integrations/twitter/list_members.md#twitter-add-list-member) | This block adds a specified user to a Twitter List owned by the authenticated user |
| [Twitter Bookmark Tweet](block-integrations/twitter/bookmark.md#twitter-bookmark-tweet) | This block bookmarks a tweet on Twitter |
| [Twitter Create List](block-integrations/twitter/manage_lists.md#twitter-create-list) | This block creates a new Twitter List for the authenticated user |
| [Twitter Delete List](block-integrations/twitter/manage_lists.md#twitter-delete-list) | This block deletes a specified Twitter List owned by the authenticated user |
| [Twitter Delete Tweet](block-integrations/twitter/manage.md#twitter-delete-tweet) | This block deletes a tweet on Twitter |
| [Twitter Follow List](block-integrations/twitter/list_follows.md#twitter-follow-list) | This block follows a specified Twitter list for the authenticated user |
| [Twitter Follow User](block-integrations/twitter/follows.md#twitter-follow-user) | This block follows a specified Twitter user |
| [Twitter Get Blocked Users](block-integrations/twitter/blocks.md#twitter-get-blocked-users) | This block retrieves a list of users blocked by the authenticating user |
| [Twitter Get Bookmarked Tweets](block-integrations/twitter/bookmark.md#twitter-get-bookmarked-tweets) | This block retrieves bookmarked tweets from Twitter |
| [Twitter Get Followers](block-integrations/twitter/follows.md#twitter-get-followers) | This block retrieves followers of a specified Twitter user |
| [Twitter Get Following](block-integrations/twitter/follows.md#twitter-get-following) | This block retrieves the users that a specified Twitter user is following |
| [Twitter Get Home Timeline](block-integrations/twitter/timeline.md#twitter-get-home-timeline) | This block retrieves the authenticated user's home timeline |
| [Twitter Get Liked Tweets](block-integrations/twitter/like.md#twitter-get-liked-tweets) | This block gets information about tweets liked by a user |
| [Twitter Get Liking Users](block-integrations/twitter/like.md#twitter-get-liking-users) | This block gets information about users who liked a tweet |
| [Twitter Get List](block-integrations/twitter/list_lookup.md#twitter-get-list) | This block retrieves information about a specified Twitter List |
| [Twitter Get List Members](block-integrations/twitter/list_members.md#twitter-get-list-members) | This block retrieves the members of a specified Twitter List |
| [Twitter Get List Memberships](block-integrations/twitter/list_members.md#twitter-get-list-memberships) | This block retrieves all Lists that a specified user is a member of |
| [Twitter Get List Tweets](block-integrations/twitter/list_tweets_lookup.md#twitter-get-list-tweets) | This block retrieves tweets from a specified Twitter list |
| [Twitter Get Muted Users](block-integrations/twitter/mutes.md#twitter-get-muted-users) | This block gets a list of users muted by the authenticating user |
| [Twitter Get Owned Lists](block-integrations/twitter/list_lookup.md#twitter-get-owned-lists) | This block retrieves all Lists owned by a specified Twitter user |
| [Twitter Get Pinned Lists](block-integrations/twitter/pinned_lists.md#twitter-get-pinned-lists) | This block returns the Lists pinned by the authenticated user |
| [Twitter Get Quote Tweets](block-integrations/twitter/quote.md#twitter-get-quote-tweets) | This block gets quote tweets for a specific tweet |
| [Twitter Get Retweeters](block-integrations/twitter/retweet.md#twitter-get-retweeters) | This block gets information about who has retweeted a tweet |
| [Twitter Get Space Buyers](block-integrations/twitter/spaces_lookup.md#twitter-get-space-buyers) | This block retrieves a list of users who purchased tickets to a Twitter Space |
| [Twitter Get Space By Id](block-integrations/twitter/spaces_lookup.md#twitter-get-space-by-id) | This block retrieves information about a single Twitter Space |
| [Twitter Get Space Tweets](block-integrations/twitter/spaces_lookup.md#twitter-get-space-tweets) | This block retrieves tweets shared in a Twitter Space |
| [Twitter Get Spaces](block-integrations/twitter/spaces_lookup.md#twitter-get-spaces) | This block retrieves information about multiple Twitter Spaces |
| [Twitter Get Tweet](block-integrations/twitter/tweet_lookup.md#twitter-get-tweet) | This block retrieves information about a specific Tweet |
| [Twitter Get Tweets](block-integrations/twitter/tweet_lookup.md#twitter-get-tweets) | This block retrieves information about multiple Tweets |
| [Twitter Get User](block-integrations/twitter/user_lookup.md#twitter-get-user) | This block retrieves information about a specified Twitter user |
| [Twitter Get User Mentions](block-integrations/twitter/timeline.md#twitter-get-user-mentions) | This block retrieves Tweets mentioning a specific user |
| [Twitter Get User Tweets](block-integrations/twitter/timeline.md#twitter-get-user-tweets) | This block retrieves Tweets composed by a single user |
| [Twitter Get Users](block-integrations/twitter/user_lookup.md#twitter-get-users) | This block retrieves information about multiple Twitter users |
| [Twitter Hide Reply](block-integrations/twitter/hide.md#twitter-hide-reply) | This block hides a reply to a tweet |
| [Twitter Like Tweet](block-integrations/twitter/like.md#twitter-like-tweet) | This block likes a tweet |
| [Twitter Mute User](block-integrations/twitter/mutes.md#twitter-mute-user) | This block mutes a specified Twitter user |
| [Twitter Pin List](block-integrations/twitter/pinned_lists.md#twitter-pin-list) | This block allows the authenticated user to pin a specified List |
| [Twitter Post Tweet](block-integrations/twitter/manage.md#twitter-post-tweet) | This block posts a tweet on Twitter |
| [Twitter Remove Bookmark Tweet](block-integrations/twitter/bookmark.md#twitter-remove-bookmark-tweet) | This block removes a bookmark from a tweet on Twitter |
| [Twitter Remove List Member](block-integrations/twitter/list_members.md#twitter-remove-list-member) | This block removes a specified user from a Twitter List owned by the authenticated user |
| [Twitter Remove Retweet](block-integrations/twitter/retweet.md#twitter-remove-retweet) | This block removes a retweet on Twitter |
| [Twitter Retweet](block-integrations/twitter/retweet.md#twitter-retweet) | This block retweets a tweet on Twitter |
| [Twitter Search Recent Tweets](block-integrations/twitter/manage.md#twitter-search-recent-tweets) | This block searches all public Tweets in Twitter history |
| [Twitter Search Spaces](block-integrations/twitter/search_spaces.md#twitter-search-spaces) | This block searches for Twitter Spaces based on specified terms |
| [Twitter Unfollow List](block-integrations/twitter/list_follows.md#twitter-unfollow-list) | This block unfollows a specified Twitter list for the authenticated user |
| [Twitter Unfollow User](block-integrations/twitter/follows.md#twitter-unfollow-user) | This block unfollows a specified Twitter user |
| [Twitter Unhide Reply](block-integrations/twitter/hide.md#twitter-unhide-reply) | This block unhides a reply to a tweet |
| [Twitter Unlike Tweet](block-integrations/twitter/like.md#twitter-unlike-tweet) | This block unlikes a tweet |
| [Twitter Unmute User](block-integrations/twitter/mutes.md#twitter-unmute-user) | This block unmutes a specified Twitter user |
| [Twitter Unpin List](block-integrations/twitter/pinned_lists.md#twitter-unpin-list) | This block allows the authenticated user to unpin a specified List |
| [Twitter Update List](block-integrations/twitter/manage_lists.md#twitter-update-list) | This block updates a specified Twitter List owned by the authenticated user |
| [Unban Subreddit User](block-integrations/misc.md#unban-subreddit-user) | Unbans a user from a subreddit |

## Communication

| Block Name | Description |
|------------|-------------|
| [Agent Mail Create Draft](block-integrations/agent_mail/drafts.md#agent-mail-create-draft) | Create a draft email for review or scheduled sending |
| [Agent Mail Create Inbox](block-integrations/agent_mail/inbox.md#agent-mail-create-inbox) | Create a new email inbox for an AI agent via AgentMail |
| [Agent Mail Create List Entry](block-integrations/agent_mail/lists.md#agent-mail-create-list-entry) | Add an email address or domain to an allow/block list |
| [Agent Mail Create Pod](block-integrations/agent_mail/pods.md#agent-mail-create-pod) | Create a new pod for multi-tenant customer isolation |
| [Agent Mail Create Pod Inbox](block-integrations/agent_mail/pods.md#agent-mail-create-pod-inbox) | Create a new email inbox within a pod |
| [Agent Mail Delete Draft](block-integrations/agent_mail/drafts.md#agent-mail-delete-draft) | Delete a draft or cancel a scheduled email |
| [Agent Mail Delete Inbox](block-integrations/agent_mail/inbox.md#agent-mail-delete-inbox) | Permanently delete an AgentMail inbox and all its messages, threads, and drafts |
| [Agent Mail Delete Inbox Thread](block-integrations/agent_mail/threads.md#agent-mail-delete-inbox-thread) | Permanently delete a conversation thread and all its messages |
| [Agent Mail Delete List Entry](block-integrations/agent_mail/lists.md#agent-mail-delete-list-entry) | Remove an email address or domain from an allow/block list to stop filtering it |
| [Agent Mail Delete Pod](block-integrations/agent_mail/pods.md#agent-mail-delete-pod) | Permanently delete a pod |
| [Agent Mail Forward Message](block-integrations/agent_mail/messages.md#agent-mail-forward-message) | Forward an email message to one or more recipients |
| [Agent Mail Get Draft](block-integrations/agent_mail/drafts.md#agent-mail-get-draft) | Retrieve a draft email to review its contents, recipients, and scheduled send status |
| [Agent Mail Get Inbox](block-integrations/agent_mail/inbox.md#agent-mail-get-inbox) | Retrieve details of an existing AgentMail inbox including its email address, display name, and configuration |
| [Agent Mail Get Inbox Thread](block-integrations/agent_mail/threads.md#agent-mail-get-inbox-thread) | Retrieve a conversation thread with all its messages |
| [Agent Mail Get List Entry](block-integrations/agent_mail/lists.md#agent-mail-get-list-entry) | Check if an email address or domain is in an allow/block list |
| [Agent Mail Get Message](block-integrations/agent_mail/messages.md#agent-mail-get-message) | Retrieve a specific email message by ID |
| [Agent Mail Get Message Attachment](block-integrations/agent_mail/attachments.md#agent-mail-get-message-attachment) | Download a file attachment from an email message |
| [Agent Mail Get Org Thread](block-integrations/agent_mail/threads.md#agent-mail-get-org-thread) | Retrieve a conversation thread by ID from anywhere in the organization, without needing the inbox ID |
| [Agent Mail Get Pod](block-integrations/agent_mail/pods.md#agent-mail-get-pod) | Retrieve details of an existing pod including its client_id mapping and metadata |
| [Agent Mail Get Thread Attachment](block-integrations/agent_mail/attachments.md#agent-mail-get-thread-attachment) | Download a file attachment from a conversation thread |
| [Agent Mail List Drafts](block-integrations/agent_mail/drafts.md#agent-mail-list-drafts) | List drafts in an AgentMail inbox |
| [Agent Mail List Entries](block-integrations/agent_mail/lists.md#agent-mail-list-entries) | List all entries in an AgentMail allow/block list |
| [Agent Mail List Inbox Threads](block-integrations/agent_mail/threads.md#agent-mail-list-inbox-threads) | List all conversation threads in an AgentMail inbox |
| [Agent Mail List Inboxes](block-integrations/agent_mail/inbox.md#agent-mail-list-inboxes) | List all email inboxes in your AgentMail organization with pagination support |
| [Agent Mail List Messages](block-integrations/agent_mail/messages.md#agent-mail-list-messages) | List messages in an AgentMail inbox |
| [Agent Mail List Org Drafts](block-integrations/agent_mail/drafts.md#agent-mail-list-org-drafts) | List all drafts across every inbox in your organization |
| [Agent Mail List Org Threads](block-integrations/agent_mail/threads.md#agent-mail-list-org-threads) | List threads across ALL inboxes in your organization |
| [Agent Mail List Pod Drafts](block-integrations/agent_mail/pods.md#agent-mail-list-pod-drafts) | List all drafts across all inboxes within a pod |
| [Agent Mail List Pod Inboxes](block-integrations/agent_mail/pods.md#agent-mail-list-pod-inboxes) | List all inboxes within a pod |
| [Agent Mail List Pod Threads](block-integrations/agent_mail/pods.md#agent-mail-list-pod-threads) | List all conversation threads across all inboxes within a pod |
| [Agent Mail List Pods](block-integrations/agent_mail/pods.md#agent-mail-list-pods) | List all tenant pods in your organization |
| [Agent Mail Reply To Message](block-integrations/agent_mail/messages.md#agent-mail-reply-to-message) | Reply to an existing email in the same conversation thread |
| [Agent Mail Send Draft](block-integrations/agent_mail/drafts.md#agent-mail-send-draft) | Send a draft immediately, converting it into a delivered message |
| [Agent Mail Send Message](block-integrations/agent_mail/messages.md#agent-mail-send-message) | Send a new email from an AgentMail inbox |
| [Agent Mail Update Draft](block-integrations/agent_mail/drafts.md#agent-mail-update-draft) | Update a draft's content, recipients, or scheduled send time |
| [Agent Mail Update Inbox](block-integrations/agent_mail/inbox.md#agent-mail-update-inbox) | Update the display name of an AgentMail inbox |
| [Agent Mail Update Message](block-integrations/agent_mail/messages.md#agent-mail-update-message) | Add or remove labels on an email message |
| [Baas Bot Join Meeting](block-integrations/baas/bots.md#baas-bot-join-meeting) | Deploy a bot to join and record a meeting |
| [Baas Bot Leave Meeting](block-integrations/baas/bots.md#baas-bot-leave-meeting) | Remove a bot from an ongoing meeting |
| [Gmail Add Label](block-integrations/google/gmail.md#gmail-add-label) | A block that adds a label to a specific email message in Gmail, creating the label if it doesn't exist |
| [Gmail Create Draft](block-integrations/google/gmail.md#gmail-create-draft) | Create draft emails in Gmail with automatic HTML detection and proper text formatting |
| [Gmail Draft Reply](block-integrations/google/gmail.md#gmail-draft-reply) | Create draft replies to Gmail threads with automatic HTML detection and proper text formatting |
| [Gmail Forward](block-integrations/google/gmail.md#gmail-forward) | Forward Gmail messages to other recipients with automatic HTML detection and proper formatting |
| [Gmail Get Profile](block-integrations/google/gmail.md#gmail-get-profile) | Get the authenticated user's Gmail profile details including email address and message statistics |
| [Gmail Get Thread](block-integrations/google/gmail.md#gmail-get-thread) | A block that retrieves an entire Gmail thread (email conversation) by ID, returning all messages with decoded bodies for reading complete conversations |
| [Gmail List Labels](block-integrations/google/gmail.md#gmail-list-labels) | A block that retrieves all labels (categories) from a Gmail account for organizing and categorizing emails |
| [Gmail Read](block-integrations/google/gmail.md#gmail-read) | A block that retrieves and reads emails from a Gmail account based on search criteria, returning detailed message information including subject, sender, body, and attachments |
| [Gmail Remove Label](block-integrations/google/gmail.md#gmail-remove-label) | A block that removes a label from a specific email message in a Gmail account |
| [Gmail Reply](block-integrations/google/gmail.md#gmail-reply) | Reply to Gmail threads with automatic HTML detection and proper text formatting |
| [Gmail Send](block-integrations/google/gmail.md#gmail-send) | Send emails via Gmail with automatic HTML detection and proper text formatting |
| [Hub Spot Engagement](block-integrations/hubspot/engagement.md#hub-spot-engagement) | Manages HubSpot engagements - sends emails and tracks engagement metrics |
| [Send Email To Me](block-integrations/communication.md#send-email-to-me) | Email yourself: sends a message to your own inbox, at your account's email address, from the platform, with no email account to sign in to |

## Developer Tools

| Block Name | Description |
|------------|-------------|
| [AllQuiet Create Incident](block-integrations/allquiet/incidents.md#allquiet-create-incident) | Creates an incident in All Quiet and pages the on-call responder |
| [AllQuiet Get Incident](block-integrations/allquiet/incident_search.md#allquiet-get-incident) | Fetches a single All Quiet incident by ID |
| [AllQuiet Get On Call](block-integrations/allquiet/on_call.md#allquiet-get-on-call) | Looks up who is on call in All Quiet, now or at a given time |
| [AllQuiet Incident Trigger](block-integrations/allquiet/triggers.md#allquiet-incident-trigger) | Triggers a graph when All Quiet posts an incident to this webhook |
| [AllQuiet List Incidents](block-integrations/allquiet/incident_search.md#allquiet-list-incidents) | Searches All Quiet incidents by status, severity, team or text |
| [AllQuiet List Teams](block-integrations/allquiet/teams.md#allquiet-list-teams) | Lists All Quiet teams and their IDs |
| [AllQuiet Update Incident](block-integrations/allquiet/incidents.md#allquiet-update-incident) | Investigates, resolves, escalates or comments on an All Quiet incident |
| [Conductor Create Session](block-integrations/conductor/create_session.md#conductor-create-session) | Start a new agent session (chat) in an existing Conductor workspace, optionally with a first prompt, and optionally wait for the agent's reply |
| [Conductor Create Workspace](block-integrations/conductor/create_workspace.md#conductor-create-workspace) | Create a Conductor cloud workspace for a project or repository, optionally start its agent with a prompt and wait for the reply |
| [Conductor Get Account](block-integrations/conductor/account.md#conductor-get-account) | Get an overview of your Conductor account in one call: who you are, the projects (repositories) you can open workspaces in, your sections and your routines |
| [Conductor Get Session](block-integrations/conductor/get_session.md#conductor-get-session) | Get a Conductor agent session: its details, whether the agent is idle, working or errored, and recent transcript messages |
| [Conductor Get Workspace](block-integrations/conductor/get_workspace.md#conductor-get-workspace) | Get everything about one Conductor workspace: details, current status, shared preview URL and its agent sessions |
| [Conductor List Workspaces](block-integrations/conductor/list_workspaces.md#conductor-list-workspaces) | List Conductor workspaces, optionally filtered by project, state, name, repository, creator or activity date |
| [Conductor Manage Routine](block-integrations/conductor/routines.md#conductor-manage-routine) | Create a Conductor routine (a saved prompt that runs a fresh agent in a project whenever its webhook URL is called) or rotate a routine's webhook secret |
| [Conductor Manage Section](block-integrations/conductor/sections.md#conductor-manage-section) | Create or delete a Conductor cloud section |
| [Conductor Manage Session](block-integrations/conductor/manage_session.md#conductor-manage-session) | Rename, cancel or archive a Conductor agent session |
| [Conductor Manage Workspace](block-integrations/conductor/manage_workspace.md#conductor-manage-workspace) | Change a Conductor workspace: rename it, archive, unarchive or sleep it, share or stop sharing a port at its public preview URL, or move it into a section |
| [Conductor Send Message](block-integrations/conductor/send_message.md#conductor-send-message) | Send a prompt to a Conductor agent session and, by default, wait for the agent to finish and return its reply |
| [Exa Code Context](block-integrations/exa/code_context.md#exa-code-context) | Search billions of GitHub repos, docs, and Stack Overflow for relevant code examples |
| [Execute Code](block-integrations/misc.md#execute-code) | Executes code in a sandbox environment with internet access |
| [Execute Code Step](block-integrations/misc.md#execute-code-step) | Execute code in a previously instantiated sandbox |
| [Github Add Label](block-integrations/github/issues.md#github-add-label) | A block that adds a label to a GitHub issue or pull request for categorization and organization |
| [Github Assign Issue](block-integrations/github/issues.md#github-assign-issue) | A block that assigns a GitHub user to an issue for task ownership and tracking |
| [Github Assign PR Reviewer](block-integrations/github/pull_requests.md#github-assign-pr-reviewer) | This block assigns a reviewer to a specified GitHub pull request |
| [Github Comment](block-integrations/github/issues.md#github-comment) | A block that posts comments on GitHub issues or pull requests using the GitHub API |
| [Github Compare Branches](block-integrations/github/repo_branches.md#github-compare-branches) | This block compares two branches or commits in a GitHub repository |
| [Github Create Check Run](block-integrations/github/checks.md#github-create-check-run) | Creates a new check run for a specific commit in a GitHub repository |
| [Github Create Comment Object](block-integrations/github/reviews.md#github-create-comment-object) | Creates a comment object for use with GitHub blocks |
| [Github Create File](block-integrations/github/repo_files.md#github-create-file) | This block creates a new file in a GitHub repository |
| [Github Create PR Review](block-integrations/github/reviews.md#github-create-pr-review) | This block creates a review on a GitHub pull request with optional inline comments |
| [Github Create Repository](block-integrations/github/repo.md#github-create-repository) | This block creates a new GitHub repository |
| [Github Create Status](block-integrations/github/statuses.md#github-create-status) | Creates a new commit status in a GitHub repository |
| [Github Delete Branch](block-integrations/github/repo_branches.md#github-delete-branch) | This block deletes a specified branch |
| [Github Discussion Trigger](block-integrations/github/triggers.md#github-discussion-trigger) | This block triggers on GitHub Discussions events |
| [Github Fork Repository](block-integrations/github/repo.md#github-fork-repository) | This block forks a GitHub repository to your account or an organization |
| [Github Get CI Results](block-integrations/github/ci.md#github-get-ci-results) | This block gets CI results for a commit or PR, with optional search for specific errors/warnings in logs |
| [Github Get Notification Thread](block-integrations/github/notifications.md#github-get-notification-thread) | This block fetches a single GitHub notification thread |
| [Github Get PR Review Comments](block-integrations/github/reviews.md#github-get-pr-review-comments) | This block gets all review comments from a GitHub pull request or from a specific review |
| [Github Get Repository Info](block-integrations/github/repo.md#github-get-repository-info) | This block retrieves metadata about a GitHub repository |
| [Github Get Repository Traffic](block-integrations/github/traffic.md#github-get-repository-traffic) | Get a GitHub repository's views and clones over the last 14 days, with unique visitors and cloners, plus its top referring sites and most viewed pages |
| [Github Get Repository Tree](block-integrations/github/repo_files.md#github-get-repository-tree) | This block lists the entire file tree of a GitHub repository recursively |
| [Github Get User Info](block-integrations/github/users.md#github-get-user-info) | This block fetches information about a GitHub user, or about the authenticated user (yourself) if no username is given |
| [Github Issues Trigger](block-integrations/github/triggers.md#github-issues-trigger) | This block triggers on GitHub issues events |
| [Github List Branches](block-integrations/github/repo_branches.md#github-list-branches) | This block lists all branches for a specified GitHub repository |
| [Github List Comments](block-integrations/github/issues.md#github-list-comments) | A block that retrieves all comments from a GitHub issue or pull request, including comment metadata and content |
| [Github List Commits](block-integrations/github/commits.md#github-list-commits) | This block lists commits on a branch in a GitHub repository |
| [Github List Discussions](block-integrations/github/repo.md#github-list-discussions) | This block lists recent discussions for a specified GitHub repository |
| [Github List Issues](block-integrations/github/issues.md#github-list-issues) | A block that retrieves a list of issues from a GitHub repository with their titles and URLs |
| [Github List Notifications](block-integrations/github/notifications.md#github-list-notifications) | This block lists GitHub notifications for the authenticated user, e |
| [Github List PR Reviewers](block-integrations/github/pull_requests.md#github-list-pr-reviewers) | This block lists the requested reviewers for a specified GitHub pull request, optionally including users who have already submitted a review |
| [Github List PR Reviews](block-integrations/github/reviews.md#github-list-pr-reviews) | This block lists all reviews for a specified GitHub pull request |
| [Github List Pull Requests](block-integrations/github/pull_requests.md#github-list-pull-requests) | This block lists all pull requests for a specified GitHub repository |
| [Github List Releases](block-integrations/github/repo.md#github-list-releases) | This block lists releases for a specified GitHub repository, optionally filtered by type and date range |
| [Github List Stargazers](block-integrations/github/repo.md#github-list-stargazers) | This block lists all users who have starred a specified GitHub repository |
| [Github List Tags](block-integrations/github/repo.md#github-list-tags) | This block lists all tags for a specified GitHub repository |
| [Github Make Branch](block-integrations/github/repo_branches.md#github-make-branch) | This block creates a new branch from a specified source branch |
| [Github Make Issue](block-integrations/github/issues.md#github-make-issue) | A block that creates new issues on GitHub repositories with a title and body content |
| [Github Make Pull Request](block-integrations/github/pull_requests.md#github-make-pull-request) | This block creates a new pull request on a specified GitHub repository |
| [Github Mark Notification Thread As Done](block-integrations/github/notifications.md#github-mark-notification-thread-as-done) | This block marks a GitHub notification thread as done, removing it from the notification inbox |
| [Github Mark Notification Thread As Read](block-integrations/github/notifications.md#github-mark-notification-thread-as-read) | This block marks a single GitHub notification thread as read |
| [Github Mark Notifications As Read](block-integrations/github/notifications.md#github-mark-notifications-as-read) | This block marks all GitHub notifications as read, optionally scoped to a single repository |
| [Github Merge Pull Request](block-integrations/github/pull_requests.md#github-merge-pull-request) | This block merges a pull request using merge, squash, or rebase |
| [Github Multi File Commit](block-integrations/github/commits.md#github-multi-file-commit) | This block creates a single commit with multiple file upsert/delete operations using the Git Trees API |
| [Github Pull Request Trigger](block-integrations/github/triggers.md#github-pull-request-trigger) | This block triggers on pull request events and outputs the event type and payload |
| [Github Read File](block-integrations/github/repo_files.md#github-read-file) | This block reads the content of a specified file from a GitHub repository |
| [Github Read Folder](block-integrations/github/repo_files.md#github-read-folder) | This block reads the content of a specified folder from a GitHub repository |
| [Github Read Issue](block-integrations/github/issues.md#github-read-issue) | A block that retrieves information about a specific GitHub issue, including its title, body content, and creator |
| [Github Read Pull Request](block-integrations/github/pull_requests.md#github-read-pull-request) | This block reads the body, title, user, changes, and full raw object of a specified GitHub pull request |
| [Github Release Trigger](block-integrations/github/triggers.md#github-release-trigger) | This block triggers on GitHub release events |
| [Github Remove Label](block-integrations/github/issues.md#github-remove-label) | A block that removes a label from a GitHub issue or pull request |
| [Github Resolve Review Discussion](block-integrations/github/reviews.md#github-resolve-review-discussion) | This block resolves or unresolves a review discussion thread on a GitHub pull request |
| [Github Search Code](block-integrations/github/repo_files.md#github-search-code) | This block searches for code in GitHub repositories |
| [Github Star Repository](block-integrations/github/repo.md#github-star-repository) | This block stars a GitHub repository |
| [Github Star Trigger](block-integrations/github/triggers.md#github-star-trigger) | This block triggers on GitHub star events |
| [Github Submit Pending Review](block-integrations/github/reviews.md#github-submit-pending-review) | This block submits a pending (draft) review on a GitHub pull request |
| [Github Unassign Issue](block-integrations/github/issues.md#github-unassign-issue) | A block that removes a user's assignment from a GitHub issue |
| [Github Unassign PR Reviewer](block-integrations/github/pull_requests.md#github-unassign-pr-reviewer) | This block unassigns a reviewer from a specified GitHub pull request |
| [Github Unsubscribe Notification Thread](block-integrations/github/notifications.md#github-unsubscribe-notification-thread) | This block unsubscribes you from a GitHub notification thread, muting future notifications unless you are mentioned again |
| [Github Update Check Run](block-integrations/github/checks.md#github-update-check-run) | Updates an existing check run in a GitHub repository |
| [Github Update Comment](block-integrations/github/issues.md#github-update-comment) | A block that updates an existing comment on a GitHub issue or pull request |
| [Github Update File](block-integrations/github/repo_files.md#github-update-file) | This block updates an existing file in a GitHub repository |
| [Instantiate Code Sandbox](block-integrations/misc.md#instantiate-code-sandbox) | Instantiate a sandbox environment with internet access in which you can execute code with the Execute Code Step block |
| [MCP Tool](block-integrations/mcp/block.md#mcp-tool) | Connect to any MCP server and execute its tools |
| [Slant3D Order Webhook](block-integrations/slant3d/webhook.md#slant3d-order-webhook) | This block triggers on Slant3D order status updates and outputs the event details, including tracking information when orders are shipped |

## Media Generation

| Block Name | Description |
|------------|-------------|
| [Add Audio To Video](block-integrations/video/add_audio.md#add-audio-to-video) | Block to attach an audio file to a video file using moviepy |
| [Loop Video](block-integrations/video/loop.md#loop-video) | Block to loop a video to a given duration or number of repeats |
| [Media Duration](block-integrations/video/duration.md#media-duration) | Block to get the duration of a media file |
| [Video Clip](block-integrations/video/clip.md#video-clip) | Extract a time segment from a video |
| [Video Concat](block-integrations/video/concat.md#video-concat) | Merge multiple video clips into one continuous video |
| [Video Download](block-integrations/video/download.md#video-download) | Download video from URL (YouTube, Vimeo, news sites, direct links) |
| [Video Text Overlay](block-integrations/video/text_overlay.md#video-text-overlay) | Add text overlay/caption to video |

## Productivity

| Block Name | Description |
|------------|-------------|
| [Google Calendar Create Event](block-integrations/google/calendar.md#google-calendar-create-event) | This block creates a new event in Google Calendar with customizable parameters |
| [Notion Append To Page](block-integrations/notion/append_to_page.md#notion-append-to-page) | Add content to the end of an existing Notion page |
| [Notion Create Page](block-integrations/notion/create_page.md#notion-create-page) | Create a new page in Notion |
| [Notion Read Database](block-integrations/notion/read_database.md#notion-read-database) | Query a Notion database with optional filtering and sorting, returning structured entries |
| [Notion Read Page](block-integrations/notion/read_page.md#notion-read-page) | Read a Notion page by its ID and return its raw JSON |
| [Notion Read Page Markdown](block-integrations/notion/read_page_markdown.md#notion-read-page-markdown) | Read a Notion page and convert it to Markdown format with proper formatting for headings, lists, links, and rich text |
| [Notion Search](block-integrations/notion/search.md#notion-search) | Search your Notion workspace for pages and databases by text query |
| [Notion Update Page Properties](block-integrations/notion/update_page_properties.md#notion-update-page-properties) | Update an existing Notion page's properties, its title among them, its icon or cover, or whether it is in the trash |
| [Todoist Close Task](block-integrations/todoist/tasks.md#todoist-close-task) | Closes a task in Todoist |
| [Todoist Create Comment](block-integrations/todoist/comments.md#todoist-create-comment) | Creates a new comment on a Todoist task or project |
| [Todoist Create Label](block-integrations/todoist/labels.md#todoist-create-label) | Creates a new label in Todoist, It will not work if same name already exists |
| [Todoist Create Project](block-integrations/todoist/projects.md#todoist-create-project) | Creates a new project in Todoist |
| [Todoist Create Task](block-integrations/todoist/tasks.md#todoist-create-task) | Creates a new task in a Todoist project |
| [Todoist Delete Comment](block-integrations/todoist/comments.md#todoist-delete-comment) | Deletes a Todoist comment |
| [Todoist Delete Label](block-integrations/todoist/labels.md#todoist-delete-label) | Deletes a personal label in Todoist |
| [Todoist Delete Project](block-integrations/todoist/projects.md#todoist-delete-project) | Deletes a Todoist project and all its contents |
| [Todoist Delete Section](block-integrations/todoist/sections.md#todoist-delete-section) | Deletes a section and all its tasks from Todoist |
| [Todoist Delete Task](block-integrations/todoist/tasks.md#todoist-delete-task) | Deletes a task in Todoist |
| [Todoist Get Comment](block-integrations/todoist/comments.md#todoist-get-comment) | Get a single comment from Todoist |
| [Todoist Get Comments](block-integrations/todoist/comments.md#todoist-get-comments) | Get all comments for a Todoist task or project |
| [Todoist Get Label](block-integrations/todoist/labels.md#todoist-get-label) | Gets a personal label from Todoist by ID |
| [Todoist Get Project](block-integrations/todoist/projects.md#todoist-get-project) | Gets details for a specific Todoist project |
| [Todoist Get Section](block-integrations/todoist/sections.md#todoist-get-section) | Gets a single section by ID from Todoist |
| [Todoist Get Shared Labels](block-integrations/todoist/labels.md#todoist-get-shared-labels) | Gets all shared labels from Todoist |
| [Todoist Get Task](block-integrations/todoist/tasks.md#todoist-get-task) | Get an active task from Todoist |
| [Todoist Get Tasks](block-integrations/todoist/tasks.md#todoist-get-tasks) | Get active tasks from Todoist |
| [Todoist List Collaborators](block-integrations/todoist/projects.md#todoist-list-collaborators) | Gets all collaborators for a specific Todoist project |
| [Todoist List Labels](block-integrations/todoist/labels.md#todoist-list-labels) | Gets all personal labels from Todoist |
| [Todoist List Projects](block-integrations/todoist/projects.md#todoist-list-projects) | Gets all projects and their details from Todoist |
| [Todoist List Sections](block-integrations/todoist/sections.md#todoist-list-sections) | Gets all sections and their details from Todoist |
| [Todoist Remove Shared Labels](block-integrations/todoist/labels.md#todoist-remove-shared-labels) | Removes all instances of a shared label |
| [Todoist Rename Shared Labels](block-integrations/todoist/labels.md#todoist-rename-shared-labels) | Renames all instances of a shared label |
| [Todoist Reopen Task](block-integrations/todoist/tasks.md#todoist-reopen-task) | Reopens a task in Todoist |
| [Todoist Update Comment](block-integrations/todoist/comments.md#todoist-update-comment) | Updates a Todoist comment |
| [Todoist Update Label](block-integrations/todoist/labels.md#todoist-update-label) | Updates a personal label in Todoist |
| [Todoist Update Project](block-integrations/todoist/projects.md#todoist-update-project) | Updates an existing project in Todoist |
| [Todoist Update Task](block-integrations/todoist/tasks.md#todoist-update-task) | Updates an existing task in Todoist |

## Logic and Control Flow

| Block Name | Description |
|------------|-------------|
| [Calculator](block-integrations/logic.md#calculator) | Performs a mathematical operation on two numbers |
| [Condition](block-integrations/logic.md#condition) | Handles conditional logic based on comparison operators |
| [Count Items](block-integrations/logic.md#count-items) | Counts the number of items in a collection |
| [Data Sampling](block-integrations/logic.md#data-sampling) | This block samples data from a given dataset using various sampling methods |
| [Exa Webset Ready Check](block-integrations/exa/websets.md#exa-webset-ready-check) | Check if webset is ready for next operation - enables conditional workflow branching |
| [If Input Matches](block-integrations/logic.md#if-input-matches) | Handles conditional logic based on comparison operators |
| [Pinecone Init](block-integrations/logic.md#pinecone-init) | Initializes a Pinecone index |
| [Pinecone Insert](block-integrations/logic.md#pinecone-insert) | Upload data to a Pinecone index |
| [Pinecone Query](block-integrations/logic.md#pinecone-query) | Queries a Pinecone index |
| [Step Through Items](block-integrations/logic.md#step-through-items) | Iterates over a list or dictionary and outputs each item |

## Input/Output

| Block Name | Description |
|------------|-------------|
| [Exa Webset Webhook](block-integrations/exa/webhook_blocks.md#exa-webset-webhook) | Receive webhook notifications for Exa webset events |
| [Generic Webhook Trigger](block-integrations/generic_webhook/triggers.md#generic-webhook-trigger) | This block will output the contents of the generic input for the webhook |
| [Read RSS Feed](block-integrations/misc.md#read-rss-feed) | Reads RSS feed entries from a given URL |
| [Send Authenticated Web Request](block-integrations/misc.md#send-authenticated-web-request) | Make an authenticated HTTP request with host-scoped credentials (JSON / form / multipart) |
| [Send Email](block-integrations/misc.md#send-email) | This block sends an email using the provided SMTP credentials |
| [Send Web Request](block-integrations/misc.md#send-web-request) | Make an HTTP request (JSON / form / multipart) |

## Agent Integration

| Block Name | Description |
|------------|-------------|
| [Agent Executor](block-integrations/misc.md#agent-executor) | Executes an existing agent inside your agent |
| [AutoPilot](block-integrations/misc.md#autopilot) | Execute tasks using AutoGPT AutoPilot with full access to platform tools (agent management, workspace files, web fetch, block execution, and more) |

## CRM Services

| Block Name | Description |
|------------|-------------|
| [Add Lead To Campaign](block-integrations/smartlead/campaign.md#add-lead-to-campaign) | Add a lead to a campaign in SmartLead |
| [Company Enrichment](block-integrations/dataforb2b/enrich.md#company-enrichment) | Look up and enrich a company using DataForB2B's B2B database — firmographics, headcount/size, industry, domain and social profiles from a company domain, name or LinkedIn URL |
| [Company Search](block-integrations/dataforb2b/search.md#company-search) | Search companies and accounts by structured filters — industry, headcount/size, location, funding, keywords — using DataForB2B's database |
| [Create Campaign](block-integrations/smartlead/campaign.md#create-campaign) | Create a campaign in SmartLead |
| [Hub Spot Company](block-integrations/hubspot/company.md#hub-spot-company) | Manages HubSpot companies - create, update, and retrieve company information |
| [Hub Spot Contact](block-integrations/hubspot/contact.md#hub-spot-contact) | Manages HubSpot contacts - create, update, and retrieve contact information |
| [People Search](block-integrations/dataforb2b/search.md#people-search) | Search people and B2B leads by structured filters — job title, company, location, industry, seniority, skills — using DataForB2B's database |
| [Profile Enrichment](block-integrations/dataforb2b/enrich.md#profile-enrichment) | Look up and enrich a professional profile from a LinkedIn URL using DataForB2B's B2B database — returns the full profile (current role, experience and skills) plus work email, personal email and GitHub |
| [Save Campaign Sequences](block-integrations/smartlead/campaign.md#save-campaign-sequences) | Save sequences within a campaign |

## AI Safety

| Block Name | Description |
|------------|-------------|
| [Nvidia Deepfake Detect](block-integrations/nvidia/deepfake.md#nvidia-deepfake-detect) | Detects potential deepfakes in images using Nvidia's AI API |

## Issue Tracking

| Block Name | Description |
|------------|-------------|
| [Linear Create Comment](block-integrations/linear/comment.md#linear-create-comment) | Creates a new comment on a Linear issue |
| [Linear Create Issue](block-integrations/linear/issues.md#linear-create-issue) | Creates a new issue on Linear |
| [Linear Get Project Issues](block-integrations/linear/issues.md#linear-get-project-issues) | Gets issues from a Linear project filtered by status and assignee |
| [Linear Search Issues](block-integrations/linear/issues.md#linear-search-issues) | Searches for issues on Linear |
| [Linear Search Projects](block-integrations/linear/projects.md#linear-search-projects) | Searches for projects on Linear |

## Hardware

| Block Name | Description |
|------------|-------------|
| [Compass AI Trigger](block-integrations/compass/triggers.md#compass-ai-trigger) | This block will output the contents of the compass transcription |
| [RMFG Event Trigger](block-integrations/rmfg/triggers.md#rmfg-event-trigger) | Triggers when an RMFG design, quote, cart or order changes |
| [RMFG Get Image](block-integrations/rmfg/images.md#rmfg-get-image) | Downloads RMFG's rendered picture of a design or part, with holes and bends labelled |

---

### Platform Getting Started (`docs/platform/getting-started.md`)

# Getting Started with AutoGPT: Self-Hosting Guide

## Introduction

This guide will help you set up AutoGPT on your own machine. There are two ways
to do it:

- **[Quick Setup with Docker](#quick-setup-with-docker)** runs the published
  image from Docker Hub: one container and no source checkout. Start here if
  you want to use AutoGPT.
- **[Manual Setup](#manual-setup)** clones the repository and builds every
  service with Docker Compose. Use it to develop AutoGPT, or to run each
  service in its own container.

{% hint style="warning" %}
**DO NOT FOLLOW ANY OUTSIDE TUTORIALS AS THEY WILL LIKELY BE OUT OF DATE**
{% endhint %}

## Quick Setup with Docker

The quickest way to self-host is the single-container image on Docker Hub,
[`significantgravitas/autogpt`](https://hub.docker.com/r/significantgravitas/autogpt).
It bundles the web app, APIs, workers, PostgreSQL, RabbitMQ, Valkey, and
FalkorDB-backed memory, and keeps all of its data in one Docker volume. The
image is meant for local and small installations, not
high-availability deployments.

### Prerequisites

You only need Docker: Docker Engine or Docker Desktop running Linux containers
on `amd64` or `arm64`. Git, Node.js, and Docker Compose are not required. Test
installations use about 5–6 GiB of memory, so plan for at least 8 GB of RAM
and about 25 GB of free disk. Docker Desktop caps its VM's memory: raise the
limit under **Settings → Resources**, or in `.wslconfig` on Windows with the
WSL 2 backend.

Install Docker from the [official Docker documentation](https://docs.docker.com/get-docker/),
start it, then verify that it is running:

```console
docker -v
docker info
```

### Running the image

```bash
docker run -d \
  --name autogpt \
  --restart unless-stopped \
  --shm-size 2g \
  --ulimit nofile=65536:65536 \
  --log-driver json-file \
  --log-opt max-size=50m \
  --log-opt max-file=5 \
  -p 127.0.0.1:3000:3000 \
  -e AUTOGPT_PUBLIC_URL=http://localhost:3000 \
  -v autogpt-data:/data \
  significantgravitas/autogpt:latest
```

`AUTOGPT_PUBLIC_URL` must match the address in your browser, so change both if
you publish a different port.

On Windows, put the command on one line in PowerShell, or run it from a WSL 2
terminal.

The first boot applies database migrations and can take several minutes. Don't
stop the container until Docker reports it as `healthy`:

```console
docker inspect --format '{{.State.Health.Status}}' autogpt
```

Then open [http://localhost:3000](http://localhost:3000), create your account,
and make it the administrator:

```bash
docker exec autogpt autogpt-admin promote you@example.com
```

Sign out and back in so your session picks up the administrator role.

Signup stays open to anyone who can reach the app, which is why the command
above publishes the port on `127.0.0.1` only. To close signup, remove the
container:

```bash
docker stop autogpt
docker rm autogpt
```

Then run the `docker run` command again with `-e AUTH_ALLOW_NEW_ACCOUNTS=false`
added. Keep the `autogpt-data` volume whenever you replace the container: it
holds your accounts, agents, memory, and generated secrets.

`latest` tracks the newest verified release. To pin a version, use an immutable
`vX.Y.Z` tag instead.

[Run AutoGPT in One Docker Container](single-container.md) covers model
providers, another port or a public URL, backups, upgrades, and
troubleshooting.

### Appliance installer

The [appliance installer](installer.md#install) wraps these steps in one command
for Linux, macOS, and Windows: it checks your Docker setup, pins the pulled
image by digest, and keeps its configuration in a private directory.

## Manual Setup

The manual setup clones the repository, builds every service from source, and
runs each one in its own container with Docker Compose. The `setup-autogpt.sh`
and `setup-autogpt.bat` scripts described in
[the installer reference](installer.md#installing-from-source-compose) run the
same steps for you and can also set up a local Ollama model.

{% hint style="warning" %}
Docker Compose publishes PostgreSQL, Redis, RabbitMQ, and every backend service
port on all network interfaces. Redis has no password, PostgreSQL and RabbitMQ
use the default passwords from this repository, and signup is open. Run the
stack on a machine and network you trust.
{% endhint %}

### Development prerequisites

The manual setup needs [Git](https://git-scm.com/downloads), Docker with
[Docker Compose](https://docs.docker.com/compose/install/), `make`, and
Python 3, which `make init-env` uses to generate your secrets. Node.js is only
needed to run the frontend outside Docker (see [Development](#development)) or
to migrate an older Supabase-based install. Verify the rest before continuing:

```console
git --version
docker -v
docker compose version
make --version
python3 --version
```

On Windows, where `make` is usually not installed, clone the repository as
below, then run `autogpt_platform\installer\setup-autogpt.bat` instead of the
remaining steps. It creates the `.env` files, generates the secrets, and starts
the stack.

### Cloning the Repository
The first step is cloning the AutoGPT repository to your computer.
To do this, open a terminal window in a folder on your computer and run:
```
git clone https://github.com/Significant-Gravitas/AutoGPT.git
```
If you get stuck, follow [this guide](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository).

Once that's complete you can continue the setup process.

### Running the AutoGPT Platform

To run the platform, follow these steps:

* Navigate to the `autogpt_platform` directory inside the AutoGPT folder:
  ```bash
   cd AutoGPT/autogpt_platform
  ```

- Create the `.env` files and generate your local secrets:

  ```
   make init-env
  ```

  This copies each `.env.default` to `.env` (for `autogpt_platform`, `backend`
  and `frontend`) and fills in the secrets that `.env.default` deliberately
  leaves blank — `ENCRYPTION_KEY`, `UNSUBSCRIBE_SECRET_KEY`, `INTERNAL_API_KEY`
  and `BETTER_AUTH_SECRET` — with values generated for your machine. Those files
  are public, so shipping working values in them would mean every install in
  the world shared one publicly-readable key. It is safe to re-run: it never
  overwrites an existing `.env` or a value you set yourself. You can then edit
  the `.env` files to add your own environment variables.

  The backend **refuses to start** while `ENCRYPTION_KEY` is empty, so run this
  before bringing the stack up.

- Run the platform services:
  ```
   docker compose up -d --build
  ```
  This command will start all the necessary backend services defined in the `docker-compose.yml` file in detached mode.

---

### 🛠️ Using the Makefile for Common Tasks

The repository includes a `Makefile` with helpful commands to streamline setup and development. You may use `make` commands as an alternative to calling Docker or scripts directly.

#### Most-used Makefile commands

Inside the `autogpt_platform` directory, you can use:

| Command                | What it Does                                                                 |
|------------------------|-------------------------------------------------------------------------------|
| `make init-env`        | Create missing `.env` files from `.env.default` (`autogpt_platform`, `backend`, and `frontend`) and generate the secrets they leave blank |
| `make start-core`      | Start the services the backend needs (Postgres, Redis, RabbitMQ, ClamAV, FalkorDB) in background and apply the database migrations |
| `make stop-core`       | Stop every running service in the stack                                       |
| `make logs-core`       | Tail the logs for core services                                               |
| `make format`          | Format & lint backend (Python) and frontend (TypeScript) code                 |
| `make migrate`         | Run backend database migrations                                               |
| `make publish-skills`  | Publish the skills catalog (marketplace skills and the expert roster) into your database; run `make load-store-agents` first if you want the experts' preload workflows too |
| `make run-backend`     | Run the backend FastAPI server                                                |
| `make run-frontend`    | Run the frontend Next.js development server                                   |

*Example usage:*
```sh
make init-env
make start-core
make migrate
make publish-skills
make run-backend
make run-frontend
```

`docker compose up` runs the migrations and the skills catalog publish for you: the `publish_skills` service downloads the public `Significant-Gravitas/skills-catalog` repository after `migrate` finishes, so the Skills Hub and the roster experts are there on first start. It needs network access to GitHub; offline, point `SKILLS_CATALOG_PATH` in `backend/.env` at a local checkout of that repository.

> `make init-env` matters when running the frontend outside Docker: Next.js
> only reads `.env` (not `.env.default`), and the frontend's embedded auth
> service needs `DATABASE_URL` and `BETTER_AUTH_SECRET` from it.

You can always check available Makefile recipes by running:
```sh
make help
```
(or just inspecting `autogpt_platform/Makefile`).

---

### Checking if the application is running

You can check if the server is running by visiting [http://localhost:3000](http://localhost:3000) in your browser.

**Notes:**

By default the application for different services run on the following ports:

- Frontend UI Server: 3000
- Backend Websocket Server: 8001
- Execution API Rest Server: 8006

### Upgrading: secrets are generated per install

`ENCRYPTION_KEY`, `UNSUBSCRIBE_SECRET_KEY` and `BETTER_AUTH_SECRET` used to
come with a value in `.env.default`, so every install that did not set its own
ran on the same three values. They are now blank there and generated for each
install, and the backend **does not start** without an `ENCRYPTION_KEY` or
with the one `.env.default` used to contain. This applies to Docker Compose
installs; the Docker Hub image generates its own secrets on first boot and
keeps them in its data volume.

A fresh install needs nothing beyond `make init-env` (or
`installer/setup-autogpt.sh` / `setup-autogpt.bat`, which do the same). An
install that already set its own values needs nothing either. Follow the steps
below if you are upgrading an install that

- has no `autogpt_platform/backend/.env`, or one without an `ENCRYPTION_KEY`
  line — it was running on the value from `.env.default`; or
- stops on startup with `ENCRYPTION_KEY is set to a value that was published…`
  or `ENCRYPTION_KEY is not set`.

Your connected integrations are encrypted with `ENCRYPTION_KEY`, so the steps
move them to the new key instead of losing them. Run everything from
`autogpt_platform/`.

1. Stop the stack:

   ```bash
   docker compose down
   ```

2. Keep the key your data is currently encrypted with. If you never set one,
   it is the value `.env.default` contained up to release `v0.7.4`:

   ```bash
   export OLD_ENCRYPTION_KEY=$(git show autogpt-platform-beta-v0.7.4:autogpt_platform/backend/.env.default \
     | grep '^ENCRYPTION_KEY=' | cut -d= -f2-)
   ```

   If you did set one and are replacing it, export that value instead, and keep
   a copy of the file outside the checkout until step 4 reports nothing
   unreadable, because step 3 removes the only other place the key is written
   down:

   ```bash
   cp backend/.env ~/autogpt-backend.env.$(date +%Y%m%d-%H%M%S)
   ```

3. Generate the new values. `make init-env` creates any missing `.env` file
   and fills in every secret whose line is present but empty; it never
   overwrites a value. So in `backend/.env` make sure these two lines exist
   with nothing after the `=`, and do the same for `BETTER_AUTH_SECRET=` in
   `frontend/.env`:

   ```
   ENCRYPTION_KEY=
   UNSUBSCRIBE_SECRET_KEY=
   ```

   Then:

   ```bash
   make init-env
   ```

   Without `make`, these are the same steps by hand. Copy a `.env.default`
   only where no `.env` exists yet:

   ```bash
   cp -n .env.default .env
   cp -n backend/.env.default backend/.env
   cp -n frontend/.env.default frontend/.env
   python3 single-container/runtime_config.py fill-env --path .env
   python3 single-container/runtime_config.py fill-env --path backend/.env
   python3 single-container/runtime_config.py fill-env --path frontend/.env
   ```

   Do not re-run `setup-autogpt.sh` or `setup-autogpt.bat` for this: they also
   start the stack, which is step 5.

4. Re-encrypt what is stored. Build the new images and bring the database up
   to date first; `migrate` starts the database on its own:

   ```bash
   docker compose build migrate rest_server
   docker compose run --rm migrate
   ```

   Then run the command, first as a dry run that only reports what it would
   change, then with `--apply` to write it. `--no-deps` keeps it from waiting
   on the rest of the stack, which it does not need:

   ```bash
   docker compose run --rm --no-deps -e OLD_ENCRYPTION_KEY rest_server cli rotate-encryption-key
   docker compose run --rm --no-deps -e OLD_ENCRYPTION_KEY rest_server cli rotate-encryption-key --apply
   ```

   It is safe to run more than once: values already under the new key are left
   alone, and a value that neither key can read is listed and not touched.
   Running the backend outside Docker, the same command is
   `poetry run cli rotate-encryption-key` in `autogpt_platform/backend`.

5. Start the stack again. If `BETTER_AUTH_SECRET` changed in step 3, first
   clear the token signing key the frontend stored under the old value: it can
   no longer be decrypted, and until it is removed nobody can reach the
   backend, even after signing in again. A new one is created on the next
   sign-in. `--build` brings the remaining services to the release you built
   in step 4:

   ```bash
   docker compose up -d --wait db
   docker compose exec db psql -U postgres -c 'DELETE FROM platform."UserAuthJwks";'
   docker compose up -d --build
   ```

Two smaller effects of the new values: unsubscribe links in emails sent before
the upgrade stop working (`UNSUBSCRIBE_SECRET_KEY`), and everyone signs in
again once (`BETTER_AUTH_SECRET`).

Do step 4 before step 5. On a new key the stored values are still in the
database but read as empty, and a user who connects an integration in that
state replaces their stored set: their other credentials are marked revoked.
If the stack already ran on the new key, stop it and run step 4 now. Whatever
nobody touched is recovered; a user who connected something in between gets
their older credentials re-encrypted but still revoked, and reconnects those.

### Upgrading an existing (Supabase-based) installation

Older versions of the platform ran authentication on a bundled Supabase
stack. If you self-hosted before the switch to the built-in auth service,
three things changed:

1. **Environment files**: refresh your `.env` files against the new
   `.env.default`s. `make init-env` copies `.env.default` → `.env` for
   `autogpt_platform`, `backend` and `frontend`, but only where no `.env`
   exists yet (it uses `cp -n`): it creates missing `.env` *files* and never
   overwrites an existing one. It does **not** merge newly-added variables
   into an `.env` you already have — for an existing install, diff each
   `.env` against its `.env.default` and copy the new keys across yourself.
   The `SUPABASE_*` URL/key variables are gone; the frontend now uses
   `BETTER_AUTH_SECRET` and `DATABASE_URL`.

   `ENCRYPTION_KEY`, `UNSUBSCRIBE_SECRET_KEY` and `BETTER_AUTH_SECRET` are no
   longer filled in by `.env.default`: follow
   [Upgrading: secrets are generated per install](#upgrading-secrets-are-generated-per-install)
   as part of this step.
2. **Database location**: the database now lives in a plain Postgres
   container (`pgvector/pgvector:pg15`) with its data in
   `autogpt_platform/data/db/data`. Your old data is untouched at
   `autogpt_platform/db/docker/volumes/db/data` but is no longer mounted.

   If you already booted the new stack while that folder was still called
   `volumes/`, move your data across before starting it again:
   ```sh
   mkdir -p autogpt_platform/data/db
   mv autogpt_platform/volumes/db/data autogpt_platform/data/db/data
   ```

   To carry the old Supabase data over, pick one of the two routes below.
   **Neither has been validated against a real old volume yet, so back up
   `autogpt_platform/db/docker/volumes/db/data` before you start.**

   The old bundled stack ran `supabase/postgres:15.8.1.049` and the new `db`
   service runs `pgvector/pgvector:pg15` — the same Postgres major, so
   reusing the data directory as-is is plausible rather than impossible. It
   is not guaranteed: a data directory is only portable between servers on
   the same major *and* with a compatible extension set /
   `shared_preload_libraries`. The Supabase image ships extensions and roles
   (`supabase_admin`, `pgjwt`, `pgsodium`, `pg_graphql`, …) that the plain
   pgvector image does not have, so a moved directory can fail to start, or
   start and then fail on objects that reference the missing extensions.

   *Fast path — reuse the data directory:*
   ```sh
   cd autogpt_platform
   docker compose down
   mkdir -p data/db
   rm -rf data/db/data                            # discards a freshly-initialised new DB
   cp -a db/docker/volumes/db/data data/db/data   # copy, so the old dir stays intact
   docker compose up -d db
   docker compose logs -f db
   ```
   On Linux the data directory is mode `0700` owned by the container's
   `postgres` user, so the copy needs `sudo cp -a` (the plain Postgres
   entrypoint fixes ownership on first boot). On Docker Desktop for
   macOS/Windows the plain `cp -a` is enough.
   It worked if the log settles on `database system is ready to accept
   connections` and your data is there:
   ```sh
   docker compose exec db psql -U postgres -c '\dn'
   docker compose exec db psql -U postgres -c 'select count(*) from platform."User"'
   ```
   It did not work if the container restart-loops with errors such as
   `could not open configuration file`, `could not access file "$libdir/…"`,
   `unrecognized configuration parameter`, `extension "…" is not available`,
   `data directory … has wrong ownership`, or `Permission denied` — Postgres
   is either missing something the Supabase image provided, or can't read the
   copied files. In that case `rm -rf data/db/data` and use the fallback.

   *Fallback — same-major dump and restore:*

   Step 1 starts a real Postgres server against your **original** data
   directory, read-write. Make sure you took the backup above first.
   ```sh
   cd autogpt_platform
   # 1. Bring the OLD image up against the OLD data directory, on a spare port.
   docker run --rm -d --name old-db -p 5433:5432 \
     -e POSTGRES_PASSWORD=your-super-secret-and-long-postgres-password \
     -v "$(pwd)/db/docker/volumes/db/data:/var/lib/postgresql/data" \
     supabase/postgres:15.8.1.049
   # 2. Dump without Supabase-owned ownership/ACL metadata.
   docker exec old-db pg_dump -U postgres -d postgres \
     --no-owner --no-privileges -Fc -f /tmp/old.dump
   docker cp old-db:/tmp/old.dump ./old.dump
   docker stop old-db
   # 3. Restore into the new db service (fresh volume).
   docker compose up -d db
   # A fresh volume runs db/init/00-init.sql, which creates an EMPTY auth.users
   # shim with only the columns the migrations need. Drop it first, or the
   # restore of your real auth.users collides with it and copies no users.
   docker compose exec db psql -U postgres -c 'DROP SCHEMA IF EXISTS auth CASCADE;'
   docker compose cp ./old.dump db:/tmp/old.dump
   docker compose exec db pg_restore -U postgres -d postgres \
     --no-owner --no-privileges /tmp/old.dump
   # 4. Confirm your accounts actually landed BEFORE migrating.
   docker compose exec db psql -U postgres -c 'select count(*) from auth.users'
   ```
   `pg_restore` reports errors for objects belonging to Supabase-only
   extensions and roles (`storage`, `realtime`, `supabase_admin`, `pgsodium`,
   …). Those are harmless. An error on **`auth.users`** is not: that table is
   where your accounts live, and the migration in step 3 below copies them out
   of it. If the count above is `0` — or `pg_restore` failed on `auth.users` —
   stop and fix the restore before continuing, or you will bring the stack up
   with no user accounts.

   Either way, finish with the migrations before bringing up the rest:
   ```sh
   docker compose run --rm migrate
   docker compose up -d
   ```
3. **User accounts and sessions**: a normal upgrade (stack stopped, then
   restarted on the new version) needs no extra step here.

    - Existing users are copied from the Supabase `auth.users` table into the
      Better Auth tables by the backend Prisma migration
      `20260716120000_copy_supabase_users_to_better_auth`, which runs as part
      of the `docker compose run --rm migrate` step above.
    - Existing browser sessions keep working because the frontend recognises
      old Supabase JWT cookies and swaps them for a Better Auth session on
      the user's next visit. Keep `SUPABASE_JWT_SECRET` set in
      `frontend/.env` for as long as you want that bridge open.
    - `frontend/scripts/migrate-supabase-auth.ts` is **optional** and only
      applies to a *live* cutover, where Supabase kept accepting signups
      while the new stack was already running. It is a re-runnable sweep for
      those stragglers; if you stopped the stack to upgrade, skip it.
      ```sh
      cd frontend && DATABASE_URL=postgresql://postgres:<password>@localhost:5432/postgres npx tsx scripts/migrate-supabase-auth.ts
      ```

A fresh install (empty database) needs none of this.

### Additional Notes

`make init-env` already generates a unique `ENCRYPTION_KEY` for your install, so
there is normally nothing to change here. To rotate it — for example if you
carried a key over from an older checkout, back when `.env.default` shipped a
working (and therefore public) one — generate a new key in python:

```python
from cryptography.fernet import Fernet;Fernet.generate_key().decode()
```

Or run the following command in the `autogpt_platform/backend` directory:

```bash
poetry run cli gen-encrypt-key
```

Then replace the value in `autogpt_platform/backend/.env` and re-encrypt the
stored integration credentials under it, with the previous value as
`OLD_ENCRYPTION_KEY` — steps 4 and 5 of
[Upgrading: secrets are generated per install](#upgrading-secrets-are-generated-per-install).
Without that step the credentials stored under the previous key are unreadable
and those integrations need reconnecting.

#### Auth transport security (JWKS over untrusted networks)

The backend verifies login tokens using signing keys it fetches from the frontend at `JWT_JWKS_URL` (`.../api/auth/jwks`). It trusts whatever keys that URL returns, so the fetch must run over a **trusted path**:

- **Plain `http` is fine** for `localhost` and for container-to-container traffic on a single host (the default `http://frontend:3000` over the Docker network) — there is no network segment for an attacker to sit on.
- **Use `https` on an untrusted network.** If you split the backend and frontend across separate machines on a LAN, or expose them publicly, a cleartext JWKS fetch can be intercepted: an attacker who swaps the published keys can forge tokens for any user. Put the frontend behind TLS (a reverse proxy), or issue **locally-trusted certificates** (e.g. [mkcert](https://github.com/FiloSottile/mkcert)), and point `JWT_JWKS_URL` at the `https://` URL.

The backend refuses to start if `JWT_JWKS_URL` is a cleartext `http://` URL pointing at a non-local host. If your network path is trusted (e.g. an isolated private LAN), set `JWKS_ALLOW_INSECURE_TRANSPORT=true` to boot anyway — a startup warning stays on record so the tradeoff is visible in logs.

This is a property of stateless JWT/JWKS verification in general, not something specific to AutoGPT. On a standard single-host Docker install you don't need to change anything.

### 📌 Windows Installation Note

When installing Docker on Windows, it is **highly recommended** to select **WSL 2** instead of Hyper-V. Using Hyper-V can cause compatibility issues with the platform's containers, leading to the `db` (Postgres) container being marked as **unhealthy**.

#### **Steps to enable WSL 2 for Docker:**
1. Install [WSL 2](https://learn.microsoft.com/en-us/windows/wsl/install).
2. Ensure that your Docker settings use WSL 2 as the default backend:
   - Open **Docker Desktop**.
   - Navigate to **Settings > General**.
   - Check **Use the WSL 2 based engine**.
3. Restart **Docker Desktop**.

#### **Already Installed Docker with Hyper-V?**
If you initially installed Docker with Hyper-V, you **don’t need to reinstall** it. You can switch to WSL 2 by following these steps:
1. Open **Docker Desktop**.
2. Go to **Settings > General**.
3. Enable **Use the WSL 2 based engine**.
4. Restart Docker.

🚨 **Warning:** Enabling WSL 2 may **erase your existing containers and build history**. If you have important containers, consider backing them up before switching.

For more details, refer to [Docker's official documentation](https://docs.docker.com/desktop/windows/wsl/).

### ⚠️ Podman Not Supported

AutoGPT requires **Docker** (Docker Desktop or Docker Engine). **Podman and podman-compose are not supported** and may cause path resolution issues, particularly on Windows.

If you see errors like:
```text
Error: the specified Containerfile or Dockerfile does not exist, ..\..\autogpt_platform\backend\Dockerfile
```

This indicates you're using Podman instead of Docker. Please install [Docker Desktop](https://docs.docker.com/desktop/) and use `docker compose` instead of `podman-compose`.


## Development

### Frontend Development

#### Running the frontend locally

To run the frontend locally, you need to have Node.js and PNPM installed on your machine.

Install [Node.js](https://nodejs.org/en/download/) to manage dependencies and run the frontend application.

Install [PNPM](https://pnpm.io/installation) to manage the frontend dependencies.

Run the service dependencies (backend, database, message queues, etc.):
```sh
docker compose --profile local up deps_backend --build --detach
```

Go to the `autogpt_platform/frontend` directory:
```sh
cd frontend
```

Install the dependencies:
```sh
pnpm install
```

Generate the API client:
```sh
pnpm generate:api-client
```

Run the frontend application:
```sh
pnpm dev
```

#### Formatting & Linting

Auto formatter and linter are set up in the project. To run them:

Format the code:
```sh
pnpm format
```

Lint the code:
```sh
pnpm lint
```
*Or for both frontend and backend, from the root:*
```sh
make format
```

#### Testing

To run the tests, you can use the following command:
```sh
pnpm test
```

### Backend Development

#### Running the backend locally

To run the backend locally, you need Python 3.11, 3.12 or 3.13 installed on your machine.

Install [Poetry](https://python-poetry.org/docs/#installation) to manage dependencies and virtual environments.

Run the backend dependencies (database, message queues, etc.):
```sh
docker compose --profile local up deps --build --detach
```
*Or equivalently with Makefile:*
```sh
make start-core
```

Go to the `autogpt_platform/backend` directory:
```sh
cd backend
```

Install the dependencies:
```sh
poetry install --with dev
```

Run the backend server:
```sh
poetry run app
```
*Or from within `autogpt_platform`:*
```sh
make run-backend
```

#### Formatting & Linting

Auto formatter and linter are set up in the project. To run them:

Format the code:
```sh
poetry run format
```

Lint the code:
```sh
poetry run lint
```
*Or format both frontend and backend at once:*
```sh
make format
```

#### Testing

To run the tests:

```sh
poetry run pytest -s
```

## Adding a New Agent Block

To add a new agent block, you need to create a new class that inherits from `Block` and provides the following information:
* All the block code should live in the `blocks` (`backend.blocks`) module.
* `input_schema`: the schema of the input data, represented by a Pydantic object.
* `output_schema`: the schema of the output data, represented by a Pydantic object.
* `run` method: the main logic of the block.
* `test_input` & `test_output`: the sample input and output data for the block, which will be used to auto-test the block.
* You can mock the functions declared in the block using the `test_mock` field for your unit tests.
* Once you finish creating the block, you can test it by running `poetry run pytest backend/blocks/test/test_block.py -s`.
* Create a Pull Request to the `dev` branch of the repository with your changes so you can share it with the community :)

---

### Content Classic Index (`docs/content/classic/index.md`)

# AutoGPT Agent

[🔧 **Setup**](setup/index.md)
&ensp;|&ensp;
[💻 **User guide**](./usage.md)
&ensp;|&ensp;
[🐙 **GitHub**](https://github.com/Significant-Gravitas/AutoGPT/tree/master/autogpt)

**Location:** `classic/original_autogpt/` in the GitHub repo

**Maintance Notice:** AutoGPT Classic is not supported from a security perspective. 
Dependencies will not be updated, nor will issues be fixed. If someone wishes to
contribute to novel development, we will give best effort merging to the changes that
pass the existing CI.

AutoGPT Classic was conceived when OpenAI published their GPT-4 model accompanied by a paper
outlining the advanced reasoning and task-solving abilities of the model. The concept
was (and still is) fairly simple: let an LLM decide what to do over and over, while
feeding the results of its actions back into the prompt. This allows the program to
iteratively and incrementally work towards its objective.

The fact that this program is able to execute actions on behalf of its user makes
it an **agent**. In the case of AutoGPT Classic, the user still has to authorize every action,
but as the project progresses we'll be able to give the agent more autonomy and only
require consent for select actions.

AutoGPT Classic is a **generalist agent**, meaning it is not designed with a specific task in
mind. Instead, it is designed to be able to execute a wide range of tasks across many
disciplines, as long as it can be done on a computer.

# AutoGPT Classic Documentation

Welcome to the AutoGPT Classic Documentation.

The AutoGPT Classic project consists of four main components:

- The [Agent](#agent) &ndash; also known as just "AutoGPT Classic"
- The [Benchmark](#benchmark) &ndash; AKA `agbenchmark`
- The [Forge](#forge)
- The [Frontend](#frontend)

To tie these together, we also have a [CLI] at the root of the project.

## 🤖 Agent

**[📖 About AutoGPT Classic](#autogpt-agent)**
&ensp;|&ensp;
**[🔧 Setup](setup/index.md)**
&ensp;|&ensp;
**[💻 Usage](./usage.md)**

The former heart of AutoGPT, and the project that kicked it all off: a semi-autonomous agent powered by LLMs to execute any task for you*.

We continue to develop this project with the goal of providing access to AI assistance to the masses, and building the future transparently and together.

- 💡 **Explore** - See what AI can do and be inspired by a glimpse of the future.

- 🚀 **Build with us** - We welcome any input, whether it's code or ideas for new features or improvements! Join us on [Discord](https://discord.gg/autogpt) and find out how you can join in on the action.

If you'd like to see what's next, check out the [AutoGPT Platform](../index.md).

<small>* it isn't quite there yet, but that is the ultimate goal that we are still pursuing</small>

---

## 🎯 Benchmark

**[🗒️ Readme](https://github.com/Significant-Gravitas/AutoGPT/blob/master/classic/benchmark/README.md)**

Measure your agent's performance! The `agbenchmark` can be used with any agent that supports the agent protocol, and the integration with the project's [CLI] makes it even easier to use with AutoGPT Classic and forge-based agents. The benchmark offers a stringent testing environment. Our framework allows for autonomous, objective performance evaluations, ensuring your agents are primed for real-world action.



- 📦 [**`agbenchmark`**](https://pypi.org/project/agbenchmark/) on Pypi

- 🔌 **Agent Protocol Standardization** - AutoGPT Classic uses the agent protocol from the AI Engineer Foundation to ensure compatibility with many agents, both from within and outside the project.

---

## 🏗️ Forge

**[📖 Introduction](../forge/get-started.md)**
&ensp;|&ensp;
**[🚀 Quickstart](https://github.com/Significant-Gravitas/AutoGPT/blob/master/QUICKSTART.md)**



Forge your own agent! The Forge is a ready-to-go template for your agent application. All the boilerplate code is already handled, letting you channel all your creativity into the things that set *your* agent apart.

- 🛠️ **Building with Ease** - We've set the groundwork so you can focus on your agent's personality and capabilities. Comprehensive tutorials are available [here](https://aiedge.medium.com/autogpt-forge-e3de53cc58ec).

---

## 💻 Frontend

**[🗒️ Readme](https://github.com/Significant-Gravitas/AutoGPT/blob/master/classic/frontend/README.md)**

An easy-to-use and open source frontend for any Agent Protocol-compliant agent.

- 🎮 **User-Friendly Interface** - Manage your agents effortlessly.

- 🔄 **Seamless Integration** - Smooth connectivity between your agent and our benchmarking system.

---

## 🔧 CLI
[CLI]: #cli

The project CLI makes it easy to use all of the components of AutoGPT Classic in the repo, separately or
together. To install its dependencies, simply run `./run setup`, and you're ready to go!

```shell
$ ./run
Usage: cli.py [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  agent      Commands to create, start and stop agents
  benchmark  Commands to start the benchmark and list tests and categories
  setup      Installs dependencies needed for your system.
```

Common commands:

* `./run agent start autogpt` &ndash; [runs](./usage.md#serve-agent-protocol-mode-with-ui) the AutoGPT Classic agent
* `./run agent create <name>` &ndash; creates a new Forge-based agent project at `agents/<name>`
* `./run benchmark start <agent>` &ndash; benchmarks the specified agent

---

🤔 Join the AutoGPT Discord server for any queries:
[discord.gg/autogpt](https://discord.gg/autogpt)

### Glossary of Terms

- **Repository**: Space where your project resides.
- **Forking**: Copying a repository under your account.
- **Cloning**: Making a local copy of a repository.
- **Agent**: The AutoGPT you'll create and develop.
- **Benchmarking**: Testing your agent's skills in the Forge.
- **Forge**: The template for building your AutoGPT agent.
- **Frontend**: The UI for tasks, logs, and task history.

---

### Content Contribute Index (`docs/content/contribute/index.md`)

# Contributing to the Docs

We welcome contributions to our documentation! Our docs are hosted on GitBook and published from GitHub.

## How It Works

- Documentation lives in the `docs/` directory and changes like any other code: through a pull request to `dev`
- The published docs match `master`. When a release reaches `master`, its `docs/` directory is copied to the `gitbook` branch, and GitBook publishes that
- The `gitbook` branch belongs to that copy. Don't open pull requests against it, and don't edit pages in GitBook: the next copy overwrites anything that isn't in `master`

## Editing Docs Locally

1. Clone the repository and create a branch from `dev`:

    ```shell
    git clone https://github.com/Significant-Gravitas/AutoGPT.git
    cd AutoGPT
    git checkout -b my-docs-change origin/dev
    ```

2. Make your changes to markdown files in `docs/`

3. Preview changes with any markdown preview tool

## Adding a New Page

1. Create a new markdown file in the appropriate `docs/` subdirectory
2. Add the new page to the relevant `SUMMARY.md` file to include it in the navigation
3. Submit a pull request to the `dev` branch

## Submitting a Pull Request

When you're ready to submit your changes, create a pull request targeting the `dev` branch. We will review your changes and merge them if appropriate. They appear on the published docs with the next release.

---

### Engineering Email Review README (`docs/engineering/email-review/README.md`)

# Transactional email comparisons

These are full-page browser screenshots of real Jinja-rendered emails, with synthetic names, amounts and links. Left is the integration base (billing PR #15345 plus dev); right is the email follow-up. No customer messages, private review documents, account identifiers or production credentials are included.

- Before source: `f8fee36faa38e2efdeaf51b9bc30ac82b7db27e2`.
- After source: `89ffd34bf36cf28705f7450aa31ebf9aa45001c5`.
- 52 variants: 28 shared design fixtures, 10 cause-generated alerts, 4 auth messages, 7 marketing reference pages, and 3 conditional billing/greeting variants. The marketing references are not sent by the application.
- Every variant has a 390px comparison. Twelve representative variants also have 640px desktop and 390px stripped-style comparisons. Each side is captured at its stated viewport width, without scaling; only a label bar and a 16px separator were added.
- The Briefing estimates are produced by each revision's actual `build_briefing` with fake RPC results. Balance forecasts and shortfall strings execute each revision's actual formatter. This exposes the original double conversion rather than hiding it behind preview-only sample text.
- Both revisions use the repository's shared scenario fixtures. The after fixtures include the new last-attempt and actual conversion dates, matching the payload changes. Plan prices are illustrative test values.
- Image requests retain their original URLs and are served with the exact repository PNG bytes. The Otto and wordmark bytes were checked against the live public URLs and match. The declared Google Fonts stylesheets/fonts were loaded when available.
- 728 browser cases cover both revisions at 640, 390 and 360px, stripped styles at 390 and 360px, images blocked at 390px, and a dark color preference at 390px. After cases have no horizontal overflow or unexpected broken images. The inline content measures 346px at a 390px viewport, including with style blocks removed.
- A browser's dark preference is a desk check, not a claim about Gmail/Outlook's proprietary recoloring. Actual inbox and phone validation remains a release check. The isolated browser context was closed and its temporary profile removed.

[Measurements](measurements.json) record the browser checks. The comparison PNGs below preserve both complete emails.

| Variant | 390px | Desktop | Styles removed |
|---|---|---|---|
| alert-0 | [Before / after](alert-0-390-normal.png) | — | — |
| alert-3 | [Before / after](alert-3-390-normal.png) | — | — |
| auth-change_email | [Before / after](auth-change_email-390-normal.png) | — | — |
| auth-reset_password | [Before / after](auth-reset_password-390-normal.png) | — | — |
| auth-set_password | [Before / after](auth-set_password-390-normal.png) | — | — |
| auth-verify_email | [Before / after](auth-verify_email-390-normal.png) | [Before / after](auth-verify_email-640-normal.png) | [Before / after](auth-verify_email-390-style-off.png) |
| briefing-attention-overflow | [Before / after](briefing-attention-overflow-390-normal.png) | — | — |
| briefing-daily | [Before / after](briefing-daily-390-normal.png) | — | — |
| briefing-monthly | [Before / after](briefing-monthly-390-normal.png) | — | — |
| briefing-quiet | [Before / after](briefing-quiet-390-normal.png) | — | — |
| briefing-rough | [Before / after](briefing-rough-390-normal.png) | — | — |
| briefing-weekly | [Before / after](briefing-weekly-390-normal.png) | [Before / after](briefing-weekly-640-normal.png) | [Before / after](briefing-weekly-390-style-off.png) |
| cause-auth-expired | [Before / after](cause-auth-expired-390-normal.png) | — | — |
| cause-awaiting-input | [Before / after](cause-awaiting-input-390-normal.png) | — | — |
| cause-awaiting-review | [Before / after](cause-awaiting-review-390-normal.png) | — | — |
| cause-block-failed | [Before / after](cause-block-failed-390-normal.png) | — | — |
| cause-continuous-error | [Before / after](cause-continuous-error-390-normal.png) | — | — |
| cause-guardrail | [Before / after](cause-guardrail-390-normal.png) | — | — |
| cause-low-balance | [Before / after](cause-low-balance-390-normal.png) | [Before / after](cause-low-balance-640-normal.png) | [Before / after](cause-low-balance-390-style-off.png) |
| cause-low-balance-no-forecast | [Before / after](cause-low-balance-no-forecast-390-normal.png) | — | — |
| cause-paused-failures | [Before / after](cause-paused-failures-390-normal.png) | — | — |
| cause-zero-balance | [Before / after](cause-zero-balance-390-normal.png) | [Before / after](cause-zero-balance-640-normal.png) | [Before / after](cause-zero-balance-390-style-off.png) |
| ended-payment-False | [Before / after](ended-payment-False-390-normal.png) | — | — |
| ended-payment-True | [Before / after](ended-payment-True-390-normal.png) | — | — |
| ops-processed | [Before / after](ops-processed-390-normal.png) | — | — |
| ops-request | [Before / after](ops-request-390-normal.png) | [Before / after](ops-request-640-normal.png) | [Before / after](ops-request-390-style-off.png) |
| payment_failed | [Before / after](payment_failed-390-normal.png) | [Before / after](payment_failed-640-normal.png) | [Before / after](payment_failed-390-style-off.png) |
| payment_final_notice | [Before / after](payment_final_notice-390-normal.png) | [Before / after](payment_final_notice-640-normal.png) | [Before / after](payment_final_notice-390-style-off.png) |
| reference-changelog | [Before / after](reference-changelog-390-normal.png) | [Before / after](reference-changelog-640-normal.png) | [Before / after](reference-changelog-390-style-off.png) |
| reference-tour-1 | [Before / after](reference-tour-1-390-normal.png) | [Before / after](reference-tour-1-640-normal.png) | [Before / after](reference-tour-1-390-style-off.png) |
| reference-tour-2 | [Before / after](reference-tour-2-390-normal.png) | — | — |
| reference-tour-3 | [Before / after](reference-tour-3-390-normal.png) | — | — |
| reference-tour-4 | [Before / after](reference-tour-4-390-normal.png) | — | — |
| reference-tour-5 | [Before / after](reference-tour-5-390-normal.png) | — | — |
| reference-tour-6 | [Before / after](reference-tour-6-390-normal.png) | — | — |
| subscription_cancelled | [Before / after](subscription_cancelled-390-normal.png) | — | — |
| subscription_resumed | [Before / after](subscription_resumed-390-normal.png) | — | — |
| subscription_welcome | [Before / after](subscription_welcome-390-normal.png) | [Before / after](subscription_welcome-640-normal.png) | [Before / after](subscription_welcome-390-style-off.png) |
| trial-canceled | [Before / after](trial-canceled-390-normal.png) | — | — |
| trial-converted | [Before / after](trial-converted-390-normal.png) | [Before / after](trial-converted-640-normal.png) | [Before / after](trial-converted-390-style-off.png) |
| trial-ended | [Before / after](trial-ended-390-normal.png) | — | — |
| trial-ending | [Before / after](trial-ending-390-normal.png) | — | — |
| trial-payment_failed | [Before / after](trial-payment_failed-390-normal.png) | — | — |
| trial-resumed | [Before / after](trial-resumed-390-normal.png) | — | — |
| trial-started | [Before / after](trial-started-390-normal.png) | [Before / after](trial-started-640-normal.png) | [Before / after](trial-started-390-style-off.png) |
| verdict-approved | [Before / after](verdict-approved-390-normal.png) | — | — |
| verdict-changes | [Before / after](verdict-changes-390-normal.png) | — | — |
| verdict-no-feedback | [Before / after](verdict-no-feedback-390-normal.png) | — | — |
| welcome-experts-enabled | [Before / after](welcome-experts-enabled-390-normal.png) | — | — |

### Conditional billing and greeting cases

These additional payment payloads are produced by the actual lifecycle handler with fake Stripe/user/queue boundaries. They show a `past_due` subscription without a retry date, an `unpaid` subscription with a hosted invoice, and an unnamed account.

- [greeting-fallback: before / after](greeting-fallback-390-normal.png)
- [payment-no-retry-date: before / after](payment-no-retry-date-390-normal.png)
- [final-hosted-invoice: before / after](final-hosted-invoice-390-normal.png)

---

### Platform Changelog README (`docs/platform/changelog/README.md`)

# Changelog

What's new in Otto, your Experts and the AutoGPT Platform. We ship updates regularly and document everything here.

Release windows reflect how changes were grouped for publication and may overlap when work spans consecutive releases.

## 2026

| Date | Highlights |
| ---- | ---------- |
| [October 8 – October 14](october-8-october-14-2026.md) | Redesigned wallet, trial cancel keeps access, verified email at signup, Chats in the collapsed sidebar, shorter replies |
| [September 24 – September 30](september-24-september-30-2026.md) | A bigger roster, Clay & Rock avatars, nested folders, multi-select answers, free trial on the paywall |
| [September 19](september-19-2026.md) | Meet your Experts: hire or raise an Expert, Expert pages, routines, Skills marketplace, Otto's desktop |
| [August 21 – September 4](august-21-september-4-2026.md) | Chat shows its work, clickable answers, redesigned emails, memory settings |
| [July 29 – August 13](july-29-august-13-2026.md) | Paste screenshots into chat, voice onboarding, Claude Sonnet 5, single-container self-hosting |
| [July 15 – July 22](july-15-july-22-2026.md) | New sidebar, composer plus menu, Telegram, workflow-building mode |
| [June 18 – July 9](june-18-july-9-2026.md) | File folders, shareable chats, Discord bot |
| [May 7 – June 10](may-7-june-10-2026.md) | AutoPilot upgrades, redesigned login, live subscriptions, rebuilt settings |
| [April 10 – May 1](april-10-may-1-2026.md) | Artifacts panel, agent briefings, AI chat in builder |
| [March 23 – April 9](march-23-april-9-2026.md) | Themed prompt categories, live timer stats, redesigned onboarding, copy your prompts |
| [March 20 – March 25](march-20-march-25-2026.md) | Import workflows from other tools, marketplace UI polish, dry-run mode, parallel AutoPilot actions |
| [March 13 – March 20](march-13-march-20-2026.md) | Refreshed marketplace, usage monitor, feedback button, personalized prompts |
| [March 5 – March 12](march-5-march-12-2026.md) | Folders, notifications, cleaner reasoning, inline outputs |
| [February 26 – March 4](february-26-march-4-2026.md) | Connect any app, share files, browse the web, run code, text-to-speech |
| [February 11 – 26](february-11-february-26-2026.md) | Telegram bots, agent folders, rebuilt flow editor, major AutoPilot upgrade |
| [January 29 – February 11](january-29-february-11-2026.md) | Voice input, persistent file workspace, Claude Opus 4.6, marketplace agent customization, rebuilt chat streaming |

---

### Content Classic Setup Index (`docs/content/classic/setup/index.md`)

# AutoGPT Agent setup

[🐋 **Set up & Run with Docker**](./docker.md)
&ensp;|&ensp;
[👷🏼 **For Developers**](./for-developers.md)

## 📋 Requirements

### Linux / macOS

- Python 3.10 or later
- Poetry ([instructions](https://python-poetry.org/docs/#installation))

### Windows (WSL)

- WSL 2
  - See also the [requirements for Linux](#linux-macos)
- [Docker Desktop](https://docs.docker.com/desktop/install/windows-install/)

### Windows

!!! attention
    We recommend setting up AutoGPT with WSL. Some things don't work exactly the same on
    Windows and we currently can't provide specialized instructions for all those cases.

- Python 3.10 or later ([instructions](https://www.tutorialspoint.com/how-to-install-python-in-windows))
- Poetry ([instructions](https://python-poetry.org/docs/#installation))
- [Docker Desktop](https://docs.docker.com/desktop/install/windows-install/)


## Setting up AutoGPT

### Getting AutoGPT

Since we don't ship AutoGPT as a desktop application, you'll need to download the
[project] from GitHub and give it a place on your computer.



- To get the latest bleeding edge version, use `master`.
- If you're looking for more stability, check out the latest AutoGPT [release][releases].

[project]: https://github.com/Significant-Gravitas/AutoGPT
[releases]: https://github.com/Significant-Gravitas/AutoGPT/releases

!!! note
    These instructions don't apply if you're looking to run AutoGPT as a docker image.
    Instead, check out the [Docker setup](./docker.md) guide.

### Completing the Setup

Once you have cloned or downloaded the project, you can find the AutoGPT Agent in the
`original_autogpt/` folder.
Inside this folder you can configure the AutoGPT application with an `.env` file and (optionally) a JSON configuration file:

- `.env` for environment variables, which are mostly used for sensitive data like API keys
- a JSON configuration file to customize certain features of AutoGPT's [Components](../../forge/components/introduction.md)

See the [Configuration](../configuration/options.md) reference for a list of available environment variables.

1. Find the file named `.env.template`. This file may
    be hidden by default in some operating systems due to the dot prefix. To reveal
    hidden files, follow the instructions for your specific operating system:
    [Windows][show hidden files/Windows] and [macOS][show hidden files/macOS].
2. Create a copy of `.env.template` and call it `.env`;
    if you're already in a command prompt/terminal window:
    ```shell
    cp .env.template .env
    ```
3. Open the `.env` file in a text editor.
4. Set API keys for the LLM providers that you want to use: see [below](#setting-up-llm-providers).
5. Enter any other API keys or tokens for services you would like to use.

    !!! note
        To activate and adjust a setting, remove the `# ` prefix.

6. Save and close the `.env` file.
7. _Optional: run `poetry install` to install all required dependencies._ The
    application also checks for and installs any required dependencies when it starts.
8. _Optional: configure the JSON file (e.g. `config.json`) with your desired settings._
    The application will use default settings if you don't provide a JSON configuration file.
    Learn how to [set up the JSON configuration file](../../forge/components/components.md#json-configuration)

You should now be able to explore the CLI (`./autogpt.sh --help`) and run the application.

See the [user guide](../usage.md) for further instructions.

[show hidden files/Windows]: https://support.microsoft.com/en-us/windows/view-hidden-files-and-folders-in-windows-97fbc472-c603-9d90-91d0-1166d1d9f4b5
[show hidden files/macOS]: https://www.pcmag.com/how-to/how-to-access-your-macs-hidden-files

## Setting up LLM providers

You can use AutoGPT with any of the following LLM providers.
Each of them comes with its own setup instructions.

AutoGPT was originally built on top of OpenAI's GPT-4, but now you can get
similar and interesting results using other models/providers too.
If you don't know which to choose, you can safely go with OpenAI*.

<small>* subject to change</small>

### OpenAI

!!! attention
    To use AutoGPT with GPT-4 (recommended), you need to set up a paid OpenAI account
    with some money in it. Please refer to OpenAI for further instructions ([link][openai/help-gpt-4-access]).
    Free accounts are [limited][openai/api-limits] to GPT-3.5 with only 3 requests per minute.

1. Make sure you have a paid account with some credits set up: [Settings > Organization > Billing][openai/billing]
1. Get your OpenAI API key from: [API keys][openai/api-keys]
2. Open `.env`
3. Find the line that says `OPENAI_API_KEY=`
4. Insert your OpenAI API Key directly after = without quotes or spaces:
    ```ini
    OPENAI_API_KEY=sk-proj-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    ```

    !!! info "Using a GPT Azure-instance"
        If you want to use GPT on an Azure instance, set `USE_AZURE` to `True` and
        make an Azure configuration file.

        Rename `azure.yaml.template` to `azure.yaml` and provide the relevant
        `azure_api_base`, `azure_api_version` and deployment IDs for the models that you
        want to use.

        E.g. if you want to use `gpt-3.5-turbo` and `gpt-4-turbo`:

        ```yaml
        # Please specify all of these values as double-quoted strings
        # Replace string in angled brackets (<>) to your own deployment Name
        azure_model_map:
            gpt-3.5-turbo: "<gpt-35-turbo-deployment-id>"
            gpt-4-turbo: "<gpt-4-turbo-deployment-id>"
            ...
        ```

        Details can be found in the [openai/python-sdk/azure], and in the [Azure OpenAI docs] for the embedding model.
        If you're on Windows you may need to install an [MSVC library](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170).

!!! important
    Keep an eye on your API costs on [the Usage page][openai/usage].


[openai/api-keys]: https://platform.openai.com/account/api-keys
[openai/billing]: https://platform.openai.com/account/billing/overview
[openai/usage]: https://platform.openai.com/account/usage
[openai/api-limits]: https://platform.openai.com/docs/guides/rate-limits/free-tier-rate-limits
[openai/help-gpt-4-access]: https://help.openai.com/en/articles/7102672-how-can-i-access-gpt-4-gpt-4-turbo-and-gpt-4o#h_9bddcd317c
[openai/python-sdk/azure]: https://github.com/openai/openai-python?tab=readme-ov-file#microsoft-azure-openai


### Anthropic

1. Make sure you have credits in your account: [Settings > Plans & billing][anthropic/billing]
2. Get your Anthropic API key from [Settings > API keys][anthropic/api-keys]
3. Open `.env`
4. Find the line that says `ANTHROPIC_API_KEY=`
5. Insert your Anthropic API Key directly after = without quotes or spaces:
    ```ini
    ANTHROPIC_API_KEY=sk-ant-api03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    ```
6. Set `SMART_LLM` and/or `FAST_LLM` to the Claude 3 model you want to use.
   See Anthropic's [models overview][anthropic/models] for info on the available models.
   Example:
    ```ini
    SMART_LLM=claude-3-opus-20240229
    ```

!!! important
    Keep an eye on your API costs on [the Usage page][anthropic/usage].

[anthropic/api-keys]: https://console.anthropic.com/settings/keys
[anthropic/billing]: https://console.anthropic.com/settings/plans
[anthropic/usage]: https://console.anthropic.com/settings/usage
[anthropic/models]: https://docs.anthropic.com/en/docs/models-overview


### Groq

!!! note
    Although Groq is supported, its built-in function calling API isn't mature.
    Any features using this API may experience degraded performance.
    Let us know your experience!

1. Get your Groq API key from [Settings > API keys][groq/api-keys]
2. Open `.env`
3. Find the line that says `GROQ_API_KEY=`
4. Insert your Groq API Key directly after = without quotes or spaces:
    ```ini
    GROQ_API_KEY=gsk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    ```
5. Set `SMART_LLM` and/or `FAST_LLM` to the Groq model you want to use.
   See Groq's [models overview][groq/models] for info on the available models.
   Example:
    ```ini
    SMART_LLM=llama3-70b-8192
    ```

[groq/api-keys]: https://console.groq.com/keys
[groq/models]: https://console.groq.com/docs/models


### Llamafile

With llamafile you can run models locally, which means no need to set up billing,
and guaranteed data privacy.

For more information and in-depth documentation, check out the [llamafile documentation].

!!! warning
    At the moment, llamafile only serves one model at a time. This means you can not
    set `SMART_LLM` and `FAST_LLM` to two different llamafile models.

!!! warning
    Due to the issues linked below, llamafiles don't work on WSL. To use a llamafile
    with AutoGPT in WSL, you will have to run the llamafile in Windows (outside WSL).

    <details>
    <summary>Instructions</summary>

    1. Get the `llamafile/serve.py` script through one of these two ways:
        1. Clone the AutoGPT repo somewhere in your Windows environment,
           with the script located at `classic/original_autogpt/scripts/llamafile/serve.py`
        2. Download just the [serve.py] script somewhere in your Windows environment
    2. Make sure you have `click` installed: `pip install click`
    3. Run `ip route | grep default | awk '{print $3}'` *inside WSL* to get the address
       of the WSL host machine
    4. Run `python3 serve.py --host {WSL_HOST_ADDR}`, where `{WSL_HOST_ADDR}`
       is the address you found at step 3.
       If port 8080 is taken, also specify a different port using `--port {PORT}`.
    5. In WSL, set `LLAMAFILE_API_BASE=http://{WSL_HOST_ADDR}:8080/v1` in your `.env`.
    6. Follow the rest of the regular instructions below.

    [serve.py]: https://github.com/Significant-Gravitas/AutoGPT/blob/master/classic/original_autogpt/scripts/llamafile/serve.py
    </details>

    * [Mozilla-Ocho/llamafile#356](https://github.com/Mozilla-Ocho/llamafile/issues/356)
    * [Mozilla-Ocho/llamafile#100](https://github.com/Mozilla-Ocho/llamafile/issues/100)

!!! note
    These instructions will download and use `mistral-7b-instruct-v0.2.Q5_K_M.llamafile`.
    `mistral-7b-instruct-v0.2` is currently the only tested and supported model.
    If you want to try other models, you'll have to add them to `LlamafileModelName` in
    [`llamafile.py`][classic/forge/llamafile.py].
    For optimal results, you may also have to add some logic to adapt the message format,
    like `LlamafileProvider._adapt_chat_messages_for_mistral_instruct(..)` does.

1. Run the llamafile serve script:
   ```shell
   python3 ./scripts/llamafile/serve.py
   ```
   The first time this is run, it will download a file containing the model + runtime,
   which may take a while and a few gigabytes of disk space.

   To force GPU acceleration, add `--use-gpu` to the command.

3. In `.env`, set `SMART_LLM`/`FAST_LLM` or both to `mistral-7b-instruct-v0.2`

4. If the server is running on different address than `http://localhost:8080/v1`,
   set `LLAMAFILE_API_BASE` in `.env` to the right base URL

[llamafile documentation]: https://github.com/Mozilla-Ocho/llamafile#readme
[classic/forge/llamafile.py]: https://github.com/Significant-Gravitas/AutoGPT/blob/master/classic/forge/llm/providers/llamafile/llamafile.py

---

### AGENTS (`docs/AGENTS.md`)

# Documentation Guidelines

## Where a Document Goes

- **Published pages** live in `docs/home/`, `docs/platform/` and `docs/integrations/`. GitBook publishes them at agpt.co/docs, and a page appears on the site once that folder's `SUMMARY.md` lists it. Put a page there only if it is written for people outside the team: people using, self-hosting or contributing to AutoGPT.
- **Engineering notes** live in `docs/engineering/`: the team's working documents, such as analytics and tracking plans, rollout plans, runbooks, internal references, and design and architecture notes. They are not on the docs site. Don't list one in a `SUMMARY.md`.

`docs/engineering/` is not private. The repository is public and AutoPilot's documentation search indexes the folder, so keep secrets and confidential material out of it, as with any file in the repository.

When unsure, write the document in `docs/engineering/`. Listing a page in a `SUMMARY.md` publishes it, so do that only for a page meant for the site.

The site follows `master`. `.github/workflows/docs-gitbook-sync.yml` copies `master`'s `docs/` to the `gitbook` branch, and GitBook publishes that branch. Change docs through a pull request to `dev`. Never commit to `gitbook`, open a pull request against it, or edit pages in GitBook: the next copy overwrites anything that isn't on `master`.

Every Markdown page under `docs/platform/` must be listed in `docs/platform/SUMMARY.md`: every `.md` file other than `SUMMARY.md` itself and GitBook's own files under a `.gitbook/` directory. A `SUMMARY.md` may list only pages inside its own folder. `.github/workflows/scripts/test_docs_layout.py` checks those two things on every pull request. It cannot tell whether a listed page is meant for the public; that is for the author and the reviewer to judge.

## Block Documentation Manual Sections

When updating manual sections (``) in block documentation files (e.g., `docs/integrations/basic.md`), follow these formats:

### How It Works Section

Provide a technical explanation of how the block functions:
- Describe the processing logic in 1-2 paragraphs
- Mention any validation, error handling, or edge cases
- Use code examples with backticks when helpful (e.g., `[[1, 2], [3, 4]]` becomes `[1, 2, 3, 4]`)

Example:
```markdown

The block iterates through each list in the input and extends a result list with all elements from each one. It processes lists in order, so `[[1, 2], [3, 4]]` becomes `[1, 2, 3, 4]`.

The block includes validation to ensure each item is actually a list. If a non-list value is encountered, the block outputs an error message instead of proceeding.

```

### Use Case Section

Provide 3 practical use cases in this format:
- **Bold Heading**: Short one-sentence description

Example:
```markdown

**Paginated API Merging**: Combine results from multiple API pages into a single list for batch processing or display.

**Parallel Task Aggregation**: Merge outputs from parallel workflow branches that each produce a list of results.

**Multi-Source Data Collection**: Combine data collected from different sources (like multiple RSS feeds or API endpoints) into one unified list.

```

### Style Guidelines

- Keep descriptions concise and action-oriented
- Focus on practical, real-world scenarios
- Use consistent terminology with other blocks
- Avoid overly technical jargon unless necessary

---

### Engineering Activation Metrics (`docs/engineering/activation-metrics.md`)

# Activation Metrics & Experiments

How the platform measures activation, retention and unit economics across
every surface (classic agents, Autopilot, experts, schedules), and how to run
an A/B/C experiment whose results can be sliced in both PostHog and Looker.

This page is the contract between engineering and go-to-market: the event
names, the SQL views and the definitions below are the ones dashboards should
be built on. Change a definition here and in every view that repeats it: the
task predicate is spelled out in `user_task_daily`, `user_lifecycle`,
`retention_task_weekly` and `unit_economics_monthly` until a shared base view
exists, so a change to one must be made in all four.

## Where each tool fits

| Tool | Owns | Reads |
| --- | --- | --- |
| **Postgres `analytics.*` views** | The numbers. Every metric below is a view in `autogpt_platform/analytics/queries/`. | Primary tables (`platform.*`, `auth.*`). |
| **Looker Studio** | GTM dashboards. | Only `analytics.*` views, via the `analytics_readonly` role. |
| **PostHog** | Funnels, session replay, experiment bucketing and significance testing. | Frontend autocapture + the server-side activation events below. Can also read `analytics.*` as a data-warehouse source. |
| **LaunchDarkly** | Feature gating (on/off, rollouts, targeting). Not for A/B measurement. | — |
| **DataFast** | Marketing attribution: which channel brought a visitor who signed up and paid. No in-app events; PostHog has those. | Its script (visits, the `/tour` goals, and `tour_cta_click` from the logged-out marketplace sidebar), the server-side `signup` goal, and its visitor ids in Stripe Checkout metadata for purchases. |
| **Admin dashboard** | Ops views: platform costs, copilot usage export, execution accuracy. | Backend admin routes. |
| **Sentry** | Errors and stack traces behind `agent_run_finished` with `status: failed`. | — |

## Definitions

- **Task**: a unit of work a person asked for *now*. Either a human-started
  agent run (`triggerSource` in `manual`, `api`) or a human chat turn (a
  `user` message in an Autopilot or expert session whose origin is not
  `automation`). A run the copilot tool started (`triggerSource` = `copilot`)
  is counted once, through the chat turn that asked for it. Dry runs, nested
  sub-graph runs and dream/memory sessions are never tasks.
- **Automated task**: a schedule fire, a webhook trigger, or a scheduled
  follow-up turn. Counted separately so "people doing things" and "agents
  running" can be compared.
- **Activated**: at least 3 tasks on at least 2 distinct days within 14 days
  of signup. (`analytics.user_lifecycle.activated`)
- **Last active**: latest of last task, last visit, last scheduled run,
  falling back to signup. Because a scheduled run counts, an account whose
  schedule fires daily is never `stale_*` or `churned_30d` while it keeps
  firing; `retention_task_weekly` deliberately ignores schedules and measures
  people coming back. Use the one that matches the question.
- **Login / visit**: a session row, from `auth.sessions` (Supabase, history
  up to the Better Auth cutover on 2026-07-30) or `platform.UserAuthSession`
  (Better Auth, everything since). Every login column reads both.
- **Stale**: no activity in 14 / 30 days (`stale_14d`, `stale_30d`).
- **Churned (30d)**: had at least one task ever, signed up more than 30 days
  ago, no activity in the last 30 days.
- **Never activated (30d)**: signed up more than 30 days ago and never did a task.
- **Cost to us**: `PlatformCostLog.costMicrodollars`, our real provider spend,
  split into agent (block runs), copilot (turns) and background (dream passes).
- **Credits charged**: `CreditTransaction` `USAGE` rows, in cents. A run
  billed to an organisation writes `OrgCreditTransaction` instead; the credit
  columns in `user_task_daily`, `user_lifecycle` and `unit_economics_monthly`
  include those rows under the user who initiated them (`initiatedByUserId`),
  while cost stays with the acting user, so per-user margin stays comparable
  for org members.

## Activation events (PostHog)

Emitted server-side by `backend/util/product_analytics.py` with
`distinct_id = user id`, so they join the frontend's `posthog.identify`.
Every event carries `environment` and `source: "platform"`.

| GTM asked for | Event | Fires when | Key properties |
| --- | --- | --- | --- |
| run_agent | `agent_run_started` without `expert_id` | A person starts an agent run (UI, API key, copilot tool). Not schedules, webhooks, sub-graphs or dry runs. | `via`, `via_ref`, `graph_id`, `graph_exec_id` |
| run_autopilot | `chat_message_sent` without `expert_id` | A person sends a message in an Autopilot chat. | `chat_session_id`, `origin` (`web`, `slack`, `telegram`, `discord`) |
| run_expert | `chat_message_sent` / `agent_run_started` with `expert_id` | A person sends a message in an expert chat or starts an expert workflow run (`kind: workflow_run`). | `expert_id`, `kind` |
| schedule_agent_run / schedule_autopilot_run / schedule_expert_run | `schedule_created` | Any schedule is registered, from any surface. | `target`: `agent` / `autopilot` / `expert`, `cron`, `is_recurring`, `schedule_id` |
| schedule_agent_ran / schedule_autopilot_ran / schedule_expert_ran | `schedule_fired` | A schedule produced work. | `target`, `schedule_id`, `graph_exec_id` or `chat_session_id` |
| (trigger) | `trigger_fired` | A webhook produced a run. | `webhook_id`, `graph_exec_id`, `target` |
| agent_fail | `agent_run_finished` with `status: failed` | A run reaches FAILED. | `via`, `failure_reason`, `expert_id` |
| — | `agent_run_finished` with `status: completed` | A run reaches COMPLETED. | `via`, `cost_cents`, `duration_seconds` |
| — | `expert_hired` | A user hires an expert from a template, from any surface. | `expert_id`, `template_id`, `surface` |
| — | `integration_connected` | A user connects a credential, by OAuth or by pasting a key. | `provider`, `credential_type`, `method` |
| — | `credential_oauth_started` | The backend issues an OAuth login URL (`GET /api/integrations/{provider}/login`). Not Codex, which signs in with a device login. | `provider` |
| — | `credential_oauth_exchange_failed` | `POST /api/integrations/{provider}/callback` returns an error, on any path. Not Codex. | `provider`, `status_code` (unset for an unexpected error), `failure_class`, `detail` (error message or class, secrets removed, at most 200 characters) |
| agent_idle, stale account | *(not events)* | Computed states, see `agent_health` and `user_lifecycle`. | — |

`credential_oauth_started`, `integration_connected` (`method: oauth`) and
`credential_oauth_exchange_failed` give started / connected / failed per
provider. They are sent by the backend so they are not held back by the
browser's analytics consent. `failure_class` says where the callback failed:

| `failure_class` | Status | Cause |
| --- | --- | --- |
| `invalid_state` | 400 | State token missing, expired, or issued for another provider. |
| `provider_unavailable` | 400 / 404 / 500 / 501 | No OAuth handler for the provider, or its client id and secret are not set. |
| `token_exchange` | 400 | The provider rejected the code, or the exchange raised. |
| `credential_merge` | 400 | The new token could not be stored on an existing credential (username or provider mismatch, managed or system credential). |

An unexpected error (not an HTTP error we raise, e.g. a database error) keeps
the class of the step it happened in, with only the exception class as
`detail` and no `status_code`: the app's exception handlers pick the response
status later (`ValueError` is a 400, for example).

A granted scope set narrower than the one requested is not a callback
failure: the credential is stored and `integration_connected` fires.

The event names follow the product analytics plan; the chat events
(`chat_tool_called`, `chat_outcome`, ...) and billing events (`topup_completed`,
`trial_*`, `subscription_*`, ...) are listed there too. Every PostHog event, its sender, its properties and whether it is live,
planned or removed is listed in the [PostHog Tracking Plan](tracking-plan.md).

## SQL views (Looker)

New or extended in this change. Existing views (`retention_login_*`,
`retention_execution_*`, `users_activities`, `platform_cost_log`, ...) are
untouched.

| View | Grain | Answers |
| --- | --- | --- |
| `graph_execution` (extended) | one row per agent run | Now also `triggerSource`, `triggerRef`, `expertId`, `failureReason`, `isDryRun`, `isSubgraphRun`. |
| `chat_turn` | one row per human chat message | Autopilot vs expert usage; the SQL twin of `chat_message_sent`. |
| `user_task_daily` | user × day | How many tasks people run, by surface and by how they started; failures; our cost; credits charged; logins. |
| `user_lifecycle` | one row per user | First/last activity per surface, first-two-weeks behaviour, cost and revenue, and the labels `activated`, `stale_*`, `churned_30d`, `never_activated_30d`. The feature table for churn analysis. |
| `user_lifecycle_funnel_weekly` | signup-week cohort | Signup → onboarded → first task in 7d → activated in 14d → schedule / expert / purchase → retained at week 4. |
| `retention_task_weekly` | cohort × lifetime week | Retention where "active" means any human task on any surface. |
| `unit_economics_monthly` | user × month | Cost to us per user per month, per task and per active day; credits charged; gross margin; by tier. |
| `agent_health` | user × library agent | Idle, never-run and failing agents. |
| `experiment_assignment` | user × experiment | The arm each user saw; join on `user_id` to split anything else by variant. |
| `user_attribution` | one row per user | Where the user came from: shared anonymous id, PostHog device id, DataFast visitor id, first landing page, UTM tags, signup method. Join to `user_lifecycle` for activation by channel. |

Apply them to a database with:

```bash
cd autogpt_platform/backend
poetry run analytics-views
```

Then add each new view as a data source in Looker Studio (the header of every
`.sql` file documents its columns and example queries). `triggerSource` is
NULL on runs created before September 2026; the views expose those as
`agent_runs_untagged` and count them as human tasks.

## One identity across LaunchDarkly, PostHog and the database

The two tools used to disagree about who a visitor is. LaunchDarkly only
knew a user after login (every logged-out visitor shared the literal key
`anonymous`, so it could not bucket pre-signup at all), while PostHog minted
its own anonymous id and merged it into the user id at identify. Nothing
tied a pre-signup arm to a post-signup person, and nothing tied either to
the DataFast visitor that marketing sees.

Now the frontend owns one first-party anonymous id
(`src/services/analytics/anonymous-id.ts`, stored in localStorage, adopting
an existing PostHog device id when there is one) and hands it to everything:

- **PostHog** is bootstrapped with it as the anonymous distinct id, so
  `identify(user.id)` merges it into the person as usual.
- **LaunchDarkly** gets it as the anonymous `user` key before login, and as
  a `device` context alongside the `user` context after login. Rules on the
  `user` kind are unchanged; a rule that buckets by `device` keeps the same
  arm across signup.
- **The database** stores it on the user at first login (`UserAttribution`,
  reported by the browser to `POST /api/analytics/attribution`) together
  with the PostHog device id, the DataFast visitor and session ids (from the
  request headers the frontend already sends), the first landing page, UTM
  tags and the signup method. `analytics.user_attribution` exposes it.

Rule of thumb: **LaunchDarkly gates, PostHog measures, the database joins.**
Server-side flag evaluation stays keyed by user id (the backend has no
device context yet), so bucket pre-login experiments on the client.

## Running an experiment

1. Create a **multivariate feature flag** in PostHog with the arm keys you want
   (`control`, `variant-a`, `variant-b`, ...) and make it an experiment there.
2. In the frontend, read it through `useExperiment(key)` from
   `src/services/experiments/useExperiment.ts`. It returns `{ variant, isResolved }`:
   render the control experience until `isResolved`, and do not fire one-shot
   side effects before then, or a late variant is mis-recorded as control.
   The hook reports the arm to `POST /api/experiments/assignments` once per
   user and experiment; the backend keeps the **first** arm it sees.
3. Backend-decided experiments can call
   `backend.data.experiments.record_assignment(user_id, key, variant, source="backend")`.
4. Read results in PostHog (exposure + the activation events above as goals),
   and in Looker by joining `analytics.experiment_assignment` to
   `user_lifecycle`, `user_task_daily` or `unit_economics_monthly`.

The onboarding paywall experiment (`subscription-pricing-page-initial-state`)
is already wired this way.

## Deploying this change

1. Run the migrations `20260902120000_add_execution_trigger_source` (adds
   `AgentGraphExecution.triggerSource` / `triggerRef`),
   `20260902120100_add_experiment_assignment` (the `ExperimentAssignment`
   table) and `20260902120200_add_user_attribution` (the `UserAttribution`
   table). Existing `analytics.*` views are unaffected: no column they select
   from changes type.
2. Deploy the backend. From that moment every run row carries its trigger and
   every activation event flows to PostHog.
3. Nick Tindle runs `poetry run analytics-views` against production right
   after the deploy that carries this change, before any Looker source is
   added. Before that, on staging: `EXPLAIN (ANALYZE, BUFFERS)` on
   `user_lifecycle`, `user_lifecycle_funnel_weekly`, `experiment_assignment`
   and `user_attribution`. `user_lifecycle` has no date bound (it is a
   per-user lifetime table), so every refresh aggregates the full history of
   `AgentGraphExecution`, `ChatMessage`, `PlatformCostLog` and
   `CreditTransaction`; if staging shows that hurting, the follow-up is a
   nightly-refreshed materialized view (a `generate_views.py` change), not a
   Looker source on the live view. Then wire the new views into Looker Studio.
4. Confirm in PostHog that `agent_run_started`, `chat_message_sent`,
   `schedule_created` and `agent_run_finished` are arriving with
   `source = platform`.

## The questions GTM will ask next

The list GTM gave us is the first layer. These are the questions that follow
once they see the data, with where the answer already lives or what is still
missing.

**Activation**

- *Which first action predicts retention: an agent run, an Autopilot chat, an
  expert, or a schedule?* — `user_lifecycle` has the first-time timestamps
  and early counts per surface; group by the earliest one and compare
  `churned_30d`.
- *How long from signup to first task, and where do people stall?* —
  `hours_to_first_task` plus the onboarding funnel view; combine with PostHog
  session replay on users with `never_activated_30d`.
- *Does connecting an integration change activation?* —
  `integrations_connected_total` and `first_integration_connected_at` in
  `user_lifecycle` (from `IntegrationCredential`), `connected_integration` in
  the funnel, and the `integration_connected` event in PostHog.
- *Which marketing channel brings activated users?* — join
  `user_attribution` (UTM tags, referrer, landing page, DataFast visitor id)
  to `user_lifecycle`. For channels only DataFast knows, export DataFast by
  visitor id and join on `datafast_visitor_id`.

**Retention and churn**

- *Are we retaining people, or just their schedules?* — `retention_task_weekly`
  (people) next to `retention_execution_weekly` (all runs incl. automated).
- *Which early behaviours separate churned from retained users?* — the
  example query on `user_lifecycle`; the next step is a proper model, which
  needs the same table exported to a notebook.
- *Who is about to churn?* — `stale_14d` users with `tasks_28d` dropping vs
  their prior 28 days; `agent_health.idle_7d` and `failing` are the earliest
  per-agent signals. **Gap**: a weekly "at-risk" list is a view away, but
  acting on it (email, in-product nudge) needs the alerts pipeline.
- *Do failures cause churn?* — `agent_runs_failed_total` and
  `agent_runs_no_credits_total` in `user_lifecycle`; `failure_reason` in
  `graph_execution`. Insufficient balance is already separated because it is
  a pricing problem, not a product problem.

**Cost and pricing**

- *What will a free trial with card on file cost us?* —
  `unit_economics_monthly` by tier and by month-of-life; the
  `PERCENTILE_CONT` example shows the tail, which is what a trial cap must
  cover.
- *Cost per task by surface and model?* — split exists by surface in
  `user_task_daily`; per-model cost is in `platform_cost_log`. **Gap**: chat
  turns are not linked to the exact cost rows (copilot cost rows carry the
  session id in `graphExecId`), so cost per *turn* is an average, not exact.
- *Gross margin per user and per tier?* — `gross_margin_usd` in
  `unit_economics_monthly`, with the caveat that credits are prepaid.
- *How much do we spend on users who never come back?* — join
  `unit_economics_monthly` to `user_lifecycle.churned_30d`.
- *Background cost (dream/memory passes) as a share of total?* —
  `background_cost_usd` in `user_task_daily` and `unit_economics_monthly`.

**Experiments**

- *Did the pricing-page arm change activation or retention, not just
  conversion?* — `experiment_assignment` joined to `user_lifecycle`.
- *Can we A/B backend behaviour (model routing, default expert, prompt)?* —
  yes via `record_assignment(source="backend")`; LaunchDarkly stays the
  gate, PostHog and the assignment table hold the arm.
- *Can the team keep using LaunchDarkly for arms?* — not in the browser
  today: the unused `useLaunchDarklyExperiment` bridge and its
  `experiment_exposed` event were removed (SECRT-2722), so a LaunchDarkly arm
  reaches the assignment table only through `record_assignment`. **Gap**: the
  backend's LaunchDarkly context has no `device` kind yet, so server-evaluated
  flags cannot bucket by the anonymous id.

**Experts and schedules**

- *Do hired experts get used after day one?* — `expert_turns_total`,
  `expert_workflow_runs_total`, `experts_active` in `user_lifecycle`;
  `expert_hired` event in PostHog for the funnel.
- *How many schedules are created and then silently stop?* — creations are in
  `ActivityEvent`, fires are in `graph_execution.triggerSource = 'schedule'`.
  **Gap**: schedules live only in the APScheduler job store, so there is no
  durable schedule row to join pauses and deletions to; a `Schedule` table
  would make schedule health first-class.

**Operational gaps to close next**

- No `lastActiveAt` on `User`; `user_lifecycle` derives it, which is fine for
  dashboards but not for real-time gating.
- Frontend PostHog is not bootstrapped with flags, so experiment variants
  resolve after first paint. `useExperiment.isResolved` papers over this;
  server-side bootstrap would remove the delay.
- DataFast only sees visitors who gave analytics consent, so it undercounts
  relative to the server-side events. Use the server-side numbers as truth
  and DataFast for channel attribution only.

---

### Engineering Codex Subscription Preview (`docs/engineering/codex-subscription-preview.md`)

# ChatGPT/Codex subscription transport preview

This private preview lets a user connect a ChatGPT account and use that
account's Codex allowance for three explicit execution paths:

- The existing **Code Generation** block through the `codex_app_server`
  transport.
- A newly created AutoPilot chat session when **ChatGPT/Codex** is the only
  connected subscription transport, or is selected from multiple connected
  subscription transports.
- An **AutoPilot** graph block with an explicitly selected ChatGPT/Codex
  connection.

`codex` is the credential provider and Codex App Server is the execution
runtime. A ChatGPT plan is not unlimited inference: these calls consume the
connected account's Codex allowance or credits and remain subject to its rate
limits. Subscription-backed usage records tokens and
`billing_mode=user_subscription`; it is not reported as AutoGPT provider USD
spend.

The preview does not route Orchestrator, shared LLM blocks, image generation,
or background system work through the connected account. Existing graphs and
AutoPilot sessions keep their stored route. New AutoPilot sessions use a
connected subscription transport automatically when exactly one is available.

## Run locally

From `autogpt_platform`:

```sh
docker compose up -d --build
```

Codex pairing and its supported execution routes are available without feature
flag environment variables. The stack mounts `/run/autogpt-codex` as a
memory-backed temporary filesystem for REST, executor, and Copilot executor
processes.

Open `http://localhost:3000`, then open **Settings > Integrations**, choose
**OpenAI**, select **ChatGPT**, and click **Sign in with ChatGPT**. In the sign-in
window, open the ChatGPT verification page and enter the one-time code.

### Test the Code Generation block

1. Add a **Code Generation** block to a graph.
2. Set **Transport** to **Codex App Server**.
3. Select the connected **ChatGPT for Codex** credential.
4. Run the graph.

Existing graphs continue to default to the OpenAI API transport. The block's
model dropdown applies to that API transport. The subscription transport uses
App Server's current account-compatible default; its live catalog can change
independently of public API model names.

### Test AutoPilot

1. Open **AutoPilot** and start a new task.
2. If ChatGPT/Codex is the only connected subscription transport, it is used
   automatically and no connection selector is shown. If multiple subscription
   transports are connected, choose the desired connection before sending the
   first message.
3. Choose Balanced or Advanced as available, then send the message. Text and
   permitted AutoGPT tool calls stream through the existing AutoPilot event
   surface.

The route and credential are stored on the new session and are immutable for
that session. Start another task to use a different connected subscription
transport. The server reloads the authoritative session route before each
queued turn; queued messages carry only the credential ID, never the OAuth
tokens. A missing, revoked, or busy Codex credential fails visibly. There is no
silent fallback to an AutoGPT-funded model or another user's credential.

The visible model tier and the server-selected execution path select a Codex
route from the shared model catalog, then validate it against the models
advertised by the connected account.

| Execution path | Balanced | Advanced |
| --- | --- | --- |
| Fast | GPT-5.6 Luna | GPT-5.6 Terra |
| Claude Agent SDK | GPT-5.6 Terra | GPT-5.6 Sol |

If the account does not expose the mapped model, the transport uses the visible
account default. File attachments, agent-building tools, and SDK sub-sessions
use the same Claude Agent SDK path as platform-funded AutoPilot.
Builder-panel-bound sessions remain platform-funded in this preview because
their persistent session is created without an AI connection selector.

The Copilot executor keeps one exclusive credential lease and one Codex runtime
per connected account, then multiplexes overlapping chats onto separate Codex
threads. Canceling or finishing one chat does not stop its siblings. This pool
is process-local: a request routed to another executor process or replica cannot
join the owner yet and still fails with the bounded, retryable
`codex_credential_busy` error. A hosted multi-replica rollout therefore needs
credential-owner routing or a dedicated bridge. The normal platform AutoPilot
route is unchanged.

### Test the AutoPilot graph block

1. Add an **AutoPilot** block to a graph.
2. Select the saved connection under **ChatGPT / Codex connection**.
3. Run the graph.

The credential field is a reference-only credential input: graph validation
checks ownership and export strips the credential ID, while the graph executor
does not hold the outer credential lease. The Copilot executor acquires the
single runtime lease when it consumes the queued turn. This preserves normal
credential rebinding without deadlocking the nested execution path. Leaving the
field empty preserves the existing platform-funded behavior.

## Claude Agent SDK transport

AutoPilot keeps the existing Claude Agent SDK and Claude Code CLI as its agent
harness. A request-scoped loopback Anthropic Messages endpoint translates model
rounds to the pinned Codex App Server's experimental `dynamicTools` protocol.
Claude Code still owns the MCP tool loop, permission hooks, builder behavior,
transcript, resume, compaction, and sub-session machinery. Codex supplies the
model response using the selected user's ChatGPT/Codex credential.

The loopback endpoint is bound to `127.0.0.1` on an ephemeral port and accepts
only a random capability generated for that turn. The capability is placed in
the Claude CLI child environment; ChatGPT access, refresh, and ID tokens are
never placed there. Anthropic text and tool-use events are mapped to the same
Claude SDK response adapter used by normal AutoPilot.

The App Server process itself is fail-closed: native shell, filesystem, web
search, apps, plugins, environments, workspace roots, and approval requests are
disabled, and its sandbox is read-only. Any side effect comes only through a
tool that Claude Code was already permitted to invoke through AutoGPT's MCP and
security hooks.

ChatGPT is reached over HTTPS: sign-in, refresh, the model catalog and
inference all run against `auth.openai.com` and `chatgpt.com`, so no Codex
binary ships in the image and there is no version to pin. The Claude Agent SDK
and its CLI are still pinned by the backend lock. Because the compatibility
surface is version-sensitive, updates require the focused protocol and Messages
conformance tests to pass before rollout.

## Verify an existing local login

To test an existing local Codex login before starting the stack, run this from
`autogpt_platform/backend`:

```sh
poetry run python -m scripts.codex_preview_smoke
```

The smoke test reads `~/.codex/auth.json` without writing to it, checks the
account and model catalog, and performs one subscription-backed turn with a tool
call over HTTPS -- failing if the model never calls the tool or returns no text.
It reports the quota the turn's response headers carried. It does consume a
small amount of the connected account's usage.

## Deploy a private cloud preview

Codex pairing and supported execution routes do not require availability flags
or frontend build-time overrides. Scope a private preview with the deployment's
ordinary access controls instead of hiding device login with environment
variables. The transport selector is driven by the connected, compatible
subscription providers: it is hidden for zero or one and shown when more than
one is available.

REST, executor, and Copilot executor must share the normal AutoGPT credential
database, encryption configuration, and Redis cluster. Give every process that
may launch App Server an isolated, memory-backed `CODEX_TEMP_ROOT`; never mount
it on a persistent volume.

Set `FRONTEND_BASE_URL`, `NEXT_PUBLIC_FRONTEND_BASE_URL`, and `BETTER_AUTH_URL`
to the exact public origin of the preview, including its `https://` scheme and
without a path. REST deliberately builds the device-login URL from this
configured origin instead of trusting forwarded host headers. Leaving a
production or localhost origin in a cloud preview sends the popup to a host
that does not have the preview's Better Auth cookie.

`NEXT_PUBLIC_FRONTEND_BASE_URL` is compiled into the frontend bundle. Pass it as
a frontend image build argument, then set `BETTER_AUTH_URL` on the frontend at
runtime and `FRONTEND_BASE_URL` on REST at runtime. The development Compose file
forwards all three exported values and keeps `http://localhost:3000` as their
local default.

Device-login status and ownership are Redis-backed, so polling and cancellation
can reach different REST replicas. The active App Server login actor is still
process-local, however: an owner-pod restart interrupts the login and another
replica cannot take it over or resume it. Keep rolling deploys away from active
sign-ins and ask the user to restart an interrupted login. Broad cloud rollout
requires moving the login actor into the dedicated bridge described below.

The containers need outbound HTTPS access to OpenAI's ChatGPT/Codex endpoints.
No callback ingress from OpenAI is required because Codex App Server polls for
device-code completion.

## Preview safety boundary

- The encrypted, USER-scoped `IntegrationCredential` row remains the source of
  truth.
- Raw ChatGPT tokens do not enter Redis, RabbitMQ, frontend responses,
  container-wide environment variables, or AutoPilot session records.
- Tokens are held in memory for the life of a turn and never written to disk:
  there is no on-disk auth home, because there is no process to hand one to.
  A client is built per turn and never cached, so one user's subscription can
  never serve another's request. Refresh is owned by the credentials manager
  and serialized behind its Redis mutex, since OpenAI rotates the refresh token
  on every exchange.
- Code Generation turns expose no dynamic tools or host workspace. AutoPilot
  exposes only the tools registered by its existing Claude SDK/MCP harness.
- Runtime reuse is scoped to the exact user and credential. Credentials are
  never pooled across users or substituted for another account.
- There is no silent platform-key fallback.

The current implementation launches Codex App Server in-process from the
ordinary REST, executor, and Copilot executor containers. Those containers
currently run as root and retain the backend services' broader database, Redis,
filesystem, and network privileges. The temporary-home and fail-closed thread
configuration reduce exposure, but they do not make this a safe boundary for a
broad external cloud rollout.

Treat the current shape as local development or a tightly access-controlled
staff/private preview only. Before enabling external users, move runtime and
device-login supervision into a dedicated unprivileged bridge with a non-root
user, read-only root filesystem, dropped capabilities, memory-only credential
homes, process and concurrency limits, narrowly restricted egress, and a
least-privilege internal credential lease interface. The bridge must also own
login actor takeover or explicit restart semantics. Complete the commercial
and policy review for hosted subscription passthrough before broader rollout.

---

### Engineering Feature Flag Audit Prompt (`docs/engineering/feature-flag-audit-prompt.md`)

# Tuesday feature-flag retirement and default audit

Use the prompt below for a weekly Tuesday task in AutoGPT. Set the run time in the task schedule using America/Chicago. This is an audit prompt; it does not create a schedule or authorize production changes.

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

### Engineering Feature Flags (`docs/engineering/feature-flags.md`)

# Feature flags

Which vendor answers a flag read is set per deployment by `FEATURE_FLAG_BACKEND`:
`posthog` (the default), `launchdarkly`, or `dual` — evaluate both, serve
LaunchDarkly's answer, and log every disagreement. Without `POSTHOG_API_KEY`
the default reads every flag as its code default, which is what a self-hosted
install and CI get. Call sites are unaffected by
the choice; they read through `evaluate_feature_flag()` on the backend and
`useGetFlag` / `useFlagStatus` on the frontend.

## Retired product rollout flags

The following features are permanent product paths. The backend and browser no
longer evaluate these keys, and their former `FORCE_FLAG_*` /
`NEXT_PUBLIC_FORCE_FLAG_*` overrides have no effect:

- `artifacts-page`
- `hire-experts`
- `skills-hub`
- `onboarding-brain-dump`
- `onboarding-expert-team`
- `graphiti-memory`
- `graphiti-communities-enabled`
- `expert-spend-approval`
- `ai-agent-execution-summary`

The sidebar and search flags, `autogpt-new-layout` and `chat-search`, were
retired separately. Authentication, ownership, user preferences, billing
configuration and provider requirements still apply. Spend approval is bypassed
when `ENABLE_CREDIT=false`. Dream passes, skill learning, voice mode, payments and
other independent rollout controls retain their existing gates.

Memory still requires FalkorDB and configured embedding/model endpoints. The
standard compose and single-container stacks include FalkorDB; custom installs
must provide it. Memory operations can fail when these services are unavailable;
removing the rollout flags does not make missing services a supported no-op.
Community rebuilds and execution summaries can perform model work.

Retired provider definitions can remain during a rolling deployment for older
releases. Removing code consumers does not delete PostHog or LaunchDarkly
configuration. Use the [Tuesday audit prompt](feature-flag-audit-prompt.md) to
identify further retirement candidates.

## One PostHog project per environment

Dev and Prod each have their own PostHog project, and each project holds its
own flags. A deployment reads its flags from the same project its analytics go
to, the one `POSTHOG_API_KEY` names, and evaluates them locally with that
project's Feature Flags Secure API Key in `POSTHOG_SECRET_KEY`. Where local
evaluation can't decide a read, the SDK asks PostHog's `/flags` endpoint
instead, unless the read is local-only, as a signed-out visitor's own read is.

A flag the code reads but the project doesn't have is served its code default.
The backend still asks `/flags` about it once, because PostHog answers some
flags there that it leaves out of the definitions (one in its newer config
format, for instance). Once `/flags` has had no answer either, the backend
serves the default without asking again for five minutes, so the read costs no
network round trip. Creating the flag takes effect at the next definitions
poll, or within those five minutes for a flag the definitions leave out.

| | Dev | Prod |
|---|---|---|
| PostHog project | [281733](https://eu.posthog.com/project/281733/feature_flags) | [117338](https://eu.posthog.com/project/117338/feature_flags) |
| `POSTHOG_API_KEY`, `POSTHOG_SECRET_KEY` (backend) | Dev project's | Prod project's |
| `NEXT_PUBLIC_POSTHOG_KEY` (Vercel) | Dev project's | Prod project's |

PostHog has no environments within a project, so a flag both deployments read
is defined in each project. The **Projects** tab on the
[flag list](https://eu.posthog.com/project/281733/feature_flags?tab=projects)
and on each flag's page shows a flag across both projects side by side.

Dev and Prod both run `FEATURE_FLAG_BACKEND=posthog`, so a flag is changed in
that environment's PostHog project and LaunchDarkly answers nothing there.

**The browser.** With `NEXT_PUBLIC_FEATURE_FLAG_BACKEND=server`, the default,
the browser gets its flags from the backend's `GET /api/feature-flags`, which evaluates
them through the same path as the backend's own reads, so the browser and the
backend give one answer from one project. A Vercel preview in `server` mode
gets the answer of the backend it calls, and a backend with no vendor answers
`configured: false`, so the browser serves its own defaults and stops asking.
The other values read flags in the browser itself: `launchdarkly` from
LaunchDarkly, and `posthog` /
`dual` from the project `NEXT_PUBLIC_POSTHOG_KEY` names. `useExperiment` reads
posthog-js in every mode.

**The `environment` property.** Every PostHog flag read the backend makes, its
own and the browser's in `server` mode, carries the person property
`environment`: the backend's `APP_ENV` (`local`, `dev` or `prod`), the same
value its analytics events carry. A release condition can use it in either
project like any other person property. It is only an evaluation input, never
stored on a person. Browser reads through posthog-js (`posthog` / `dual` mode)
don't carry it: they send `buildFlagPersonProperties` (email, email domain,
role, created at) only, so a condition on `environment` matches only the
backend's reads there.

**Splitting signed-out visitors.** A visitor who allowed analytics is bucketed
under `visitor:<their PostHog id>`, but only by conditions their read can
decide: a rollout percentage, `environment` and `is_anonymous`, with no email.
A condition group on another person property or a cohort (the migrated
"email in employees", for one) can't be decided for a visitor, so a visitor
whom no decidable group matches gets the shared signed-out answer instead.
"Employees OR 20%" is then all or nothing for visitors, whichever side the
shared `anonymous` key lands on. For a real 20% of visitors, add
`is_anonymous` is not `true` to the rollout group: the shared answer is then
off, and only each visitor's own bucket turns the flag on.

## Sharing PostHog's flag definitions

PostHog evaluates flags locally, in the process, from a set of flag definitions
it polls for every 30 seconds. PostHog
[bills one definitions fetch as ten flag requests](https://posthog.com/docs/feature-flags/local-evaluation),
so a poller in every process makes that bill scale with replica count: at two
polls a minute, each process costs 28,800 flag requests a day whether or not it
reads a single flag.

So one process is elected to refresh the definitions and write them to Redis,
and every other process evaluates from that shared copy. The fleet then costs
one process's worth of polling — 28,800 flag requests a day in total, instead of
28,800 per replica.

This is PostHog's own `FlagDefinitionCacheProvider` interface; the SDK calls it
from its polling thread, never from a flag read, so no request can block on
Redis.

### Settings

| Setting | Default | What it does |
|---|---|---|
| `POSTHOG_FLAG_DEFINITION_CACHE` | `redis` | `redis` shares definitions through Redis; `memory` keeps them in the process; `none` restores one poller per process. |
| `POSTHOG_FLAG_DEFINITION_REFRESH_SECONDS` | `30` | How often the refresher fetches, and how often everyone else re-reads the shared copy. |
| `POSTHOG_FLAG_DEFINITION_CACHE_TTL_SECONDS` | `600` | How long the shared copy stays readable after the last refresh. |

None of them is read unless `FEATURE_FLAG_BACKEND` is `posthog` or `dual` **and**
`POSTHOG_SECRET_KEY` is set — without that key the SDK evaluates over the
wire and has no definitions to share.

**Per environment.** Leave the defaults on `dev` and `prod`: both run several
replicas against a shared Redis, which is what the cache is for. Local
development and CI have no PostHog keys, so none of this is reached; set `POSTHOG_FLAG_DEFINITION_CACHE=memory` if you want to exercise
PostHog locally without Redis.

### What the refresher is

Every process tries `SET <key> <id> NX PX` on one Redis key each time it polls.
The one that gets it fetches the definitions from PostHog and writes them to
Redis; the others read that copy. The winner renews its own lock on every poll,
so it keeps the job. The lock expires two poll intervals after its last renewal,
and the next process to poll takes it, so a refresher that dies is replaced
within 90 seconds at the defaults. One shutting down cleanly releases the lock
immediately, and is replaced on the next poll.

### Telling it is working

One process logs, once, when it takes the job:

```
This process now refreshes the shared PostHog flag definitions (<host>:<pid>:<id>)
```

and the others log `Another process now refreshes the shared PostHog flag
definitions` when they lose it.

The counter `autogpt_posthog_flag_definition_cache_events_total` on `/metrics`
carries the same story, by `outcome`:

| outcome | meaning |
|---|---|
| `refresher` / `stored` | this process holds the lock and wrote its definitions to the shared copy, which it does every poll whether or not PostHog reported a change |
| `follower` / `cached` | this process read the shared copy |
| `stale` | the shared copy has not been rewritten for five poll intervals, so no refresher is writing it; it is served anyway |
| `empty` | nothing in the cache; a process with no definitions yet fetches from PostHog directly, and one that has some keeps them |
| `error` | Redis was unreachable |

Healthy, with N replicas: `stored` increments once per poll on exactly one
process, `cached` once per poll on each of the other N−1, and `empty` and
`error` stay flat.

### Failure modes

| What happens | What the platform does |
|---|---|
| Cache empty at boot | The process fetches from PostHog directly, once, and the refresher fills the cache. |
| Redis unreachable | Every process polls PostHog for itself, as it would without the cache, and logs once per five minutes. Flag reads are unaffected. |
| Shared copy goes stale | It is served anyway, with a warning, while the refresher catches up. Stale definitions beat none. |
| The refresher dies | Its lock expires and another process takes the job on its next poll. |
| A definitions fetch fails | The definitions already in memory stay in use, and the refresher keeps writing that copy to the cache. |

Nothing here can make a flag read wait on Redis: evaluation reads the
definitions already in the process, and only a read they can't decide waits on
PostHog's `/flags`.

---

### Engineering Frontend Perf Baseline (`docs/engineering/frontend-perf-baseline.md`)

# Frontend performance baseline

Medians of `pnpm perf` for every scenario on `dev`, saved so later work has something to compare against. How to produce and read these tables: [frontend-perf-measurement.md](frontend-perf-measurement.md).

| | |
| --- | --- |
| App code | `dev` at `03dfdcabf` (2026-10-19). Measured on the branch that added the tooling, which changes no app behaviour. |
| Build | `NEXT_PUBLIC_RENDER_COUNTER=1 pnpm dev` (Next 15.5 with Turbopack, React 18.3 development build) |
| Browser | Playwright Chromium headless shell 141, 1440 × 900, `en-US`, UTC |
| Machine | Linux cloud sandbox, 8 vCPU Intel Xeon 2.9 GHz, 16 GB RAM, Node 24 |
| Runs | 1 warm-up, then 3 saved runs per scenario (`--label baseline`) |

Absolute timings depend on the machine, so compare a branch against these numbers only on similar hardware. To compare against your own machine, run the same scenarios on `dev` first with `--label baseline`. Render counts and commits do not depend on the machine.

What stands out:

- **Streaming a reply re-renders the whole conversation.** In `copilot-stream-reply`, one reply costs about 210,000 renders and 5.7 s of script time over about 55 commits. The biggest commits render more than 11,000 components: the 200 existing rows, mostly their tooltips, icons and Radix wrappers. Most of the cost comes after the stream ends, when the page refetches the session and re-renders the transcript several times. `copilot-idle` shows the same chat at rest: about 40 renders per commit.
- **The end of a turn is not fully deterministic.** One run in three to five does one fewer full-transcript re-render (about 11,000 renders, 5%), whatever the run's timing. The median absorbs it; use `--runs 5` when a change to this scenario is close to that size.
- **Switching threads is the most expensive scenario:** about 270,000 renders and 11.7 s of script time over nine switches.
- **Dragging a node during a running execution** renders about 500 components per pointer move, with 39 long tasks in 2 s.
- **The library with 20 running workflows** and **Home with a reply streaming in the background** are quiet.

## builder-drag-running · baseline — median of 3 runs

| metric | median | range | spread |
| --- | --- | --- | --- |
| commits | 251 | 249 – 258 | 3.6% |
| total renders | 128,816 | 128,768 – 128,857 | 0.1% |
| renders per commit | 513.2 | 499.4 – 517.1 | 3.4% |
| max renders in one commit | 11,029 | 11,029 – 11,029 | 0.0% |
| script time (ms) | 5,327 | 5,254 – 5,354 | 1.9% |
| task time (ms) | 6,811 | 6,676 – 6,838 | 2.4% |
| layout time (ms) | 37 | 37 – 38 | 3.3% |
| style recalc time (ms) | 112 | 112 – 115 | 2.8% |
| long tasks (≥ 50 ms) | 39 | 38 – 47 | 23.1% |
| long task total (ms) | 5,319 | 5,220 – 5,567 | 6.5% |
| heap used after GC (MB) | 103.5 | 103.4 – 103.6 | 0.2% |

**Top components (median renders)**

| name | renders |
| --- | --- |
| `Presence` | 6,501 |
| `Text` | 6,150 |
| `HugeiconsIcon` | 5,560 |
| `Icon` | 5,560 |
| `Tooltip` | 5,023 |
| `TooltipContent` | 5,021 |
| `TooltipProvider` | 4,949 |
| `Primitive.div` | 4,275 |
| `Popper` | 3,608 |
| `PopperAnchor` | 3,608 |

**Top render causes (median renders)**

| name | renders |
| --- | --- |
| `Presence props:children` | 6,501 |
| `HugeiconsIcon parent-same-props<-Icon` | 5,560 |
| `Tooltip props:children` | 5,023 |
| `TooltipProvider props:children` | 4,949 |
| `Primitive.div props:children` | 3,489 |
| `Primitive.div.Slot props:children` | 3,096 |
| `Primitive.div.SlotClone props:children` | 3,096 |
| `TooltipContent props:children` | 2,876 |
| `Primitive.button.Slot props:children,onPointerMove,onPointerLeave,onPointerDown,+4` | 2,654 |
| `Primitive.button.SlotClone props:onPointerMove,onPointerLeave,onPointerDown,onFocus,+4` | 2,654 |

## copilot-background-stream · baseline — median of 3 runs

| metric | median | range | spread |
| --- | --- | --- | --- |
| commits | 6 | 6 – 6 | 0.0% |
| total renders | 443 | 443 – 443 | 0.0% |
| renders per commit | 73.8 | 73.8 – 73.8 | 0.0% |
| max renders in one commit | 275 | 275 – 275 | 0.0% |
| script time (ms) | 62 | 53 – 71 | 30.1% |
| task time (ms) | 230 | 229 – 294 | 28.3% |
| layout time (ms) | 0 | 0 – 0 | 0.0% |
| style recalc time (ms) | 0 | 0 – 0 | 0.0% |
| long tasks (≥ 50 ms) | 1 | 1 – 1 | 0.0% |
| long task total (ms) | 73 | 65 – 81 | 21.5% |
| heap used after GC (MB) | 56.0 | 55.9 – 56.3 | 0.7% |

**Top components (median renders)**

| name | renders |
| --- | --- |
| `Text` | 35 |
| `motion.div` | 21 |
| `Card` | 18 |
| `motion.g` | 18 |
| `Presence` | 18 |
| `HugeiconsIcon` | 15 |
| `Icon` | 15 |
| `Primitive.div` | 13 |
| `Primitive.button` | 12 |
| `Popper` | 11 |

**Top render causes (median renders)**

| name | renders |
| --- | --- |
| `motion.div props:initial,animate,transition,children` | 21 |
| `Card parent-same-props<-motion.g` | 18 |
| `motion.g props:initial,animate,transition,children` | 18 |
| `Presence props:children` | 18 |
| `HugeiconsIcon parent-same-props<-Icon` | 15 |
| `Primitive.div props:children` | 12 |
| `Text parent-same-props<-motion.div` | 12 |
| `Primitive.div.Slot props:children` | 11 |
| `Primitive.div.SlotClone props:children` | 11 |
| `Tooltip props:children` | 10 |

## copilot-idle · baseline — median of 3 runs

| metric | median | range | spread |
| --- | --- | --- | --- |
| commits | 5 | 5 – 5 | 0.0% |
| total renders | 201 | 201 – 201 | 0.0% |
| renders per commit | 40.2 | 40.2 – 40.2 | 0.0% |
| max renders in one commit | 100 | 100 – 100 | 0.0% |
| script time (ms) | 22 | 21 – 22 | 4.1% |
| task time (ms) | 52 | 49 – 52 | 5.4% |
| layout time (ms) | 0 | 0 – 0 | 0.0% |
| style recalc time (ms) | 0 | 0 – 0 | 0.0% |
| long tasks (≥ 50 ms) | 0 | 0 – 0 | 0.0% |
| long task total (ms) | 0 | 0 – 0 | 0.0% |
| heap used after GC (MB) | 83.7 | 83.6 – 84.8 | 1.4% |

**Top components (median renders)**

| name | renders |
| --- | --- |
| `Presence` | 12 |
| `Primitive.div` | 10 |
| `Primitive.button` | 8 |
| `Anonymous` | 6 |
| `HugeiconsIcon` | 6 |
| `Icon` | 6 |
| `Popper` | 6 |
| `PopperAnchor` | 6 |
| `PopperProvider` | 6 |
| `Primitive.button.Slot` | 6 |

**Top render causes (median renders)**

| name | renders |
| --- | --- |
| `Presence props:children` | 12 |
| `Primitive.div props:children` | 8 |
| `HugeiconsIcon parent-same-props<-Icon` | 6 |
| `Primitive.div.Slot props:children` | 6 |
| `Primitive.div.SlotClone props:children` | 6 |
| `Tooltip props:children` | 6 |
| `LinkComponent props:children,onPointerMove,onPointerLeave,onPointerDown,+4` | 4 |
| `MenuProvider props:scope,children` | 4 |
| `Popper props:children` | 4 |
| `PopperAnchor props:children` | 4 |

## copilot-stream-reply · baseline — median of 3 runs

| metric | median | range | spread |
| --- | --- | --- | --- |
| commits | 53 | 52 – 55 | 5.7% |
| total renders | 209,396 | 198,389 – 209,496 | 5.3% |
| renders per commit | 3,815.2 | 3,809.0 – 3,950.9 | 3.7% |
| max renders in one commit | 11,149 | 11,149 – 11,149 | 0.0% |
| script time (ms) | 5,709 | 5,396 – 5,760 | 6.4% |
| task time (ms) | 6,158 | 5,842 – 6,252 | 6.7% |
| layout time (ms) | 20 | 19 – 24 | 22.8% |
| style recalc time (ms) | 186 | 185 – 202 | 9.5% |
| long tasks (≥ 50 ms) | 18 | 17 – 18 | 5.6% |
| long task total (ms) | 5,867 | 5,549 – 5,931 | 6.5% |
| heap used after GC (MB) | 86.4 | 85.9 – 86.6 | 0.9% |

**Top components (median renders)**

| name | renders |
| --- | --- |
| `Tooltip` | 16,230 |
| `TooltipContent` | 16,228 |
| `HugeiconsIcon` | 9,061 |
| `Icon` | 9,061 |
| `Presence` | 8,299 |
| `Primitive.div` | 8,196 |
| `Popper` | 8,194 |
| `PopperAnchor` | 8,194 |
| `PopperProvider` | 8,194 |
| `Primitive.div.Slot` | 8,194 |

**Top render causes (median renders)**

| name | renders |
| --- | --- |
| `Tooltip props:children` | 16,183 |
| `HugeiconsIcon parent-same-props<-Icon` | 9,054 |
| `Presence props:children` | 8,293 |
| `Primitive.div props:children` | 8,188 |
| `Primitive.div.Slot props:children` | 8,187 |
| `Primitive.div.SlotClone props:children` | 8,187 |
| `PopperAnchor props:children` | 8,169 |
| `Popper props:children` | 8,164 |
| `PopperProvider props:children` | 8,164 |
| `TooltipProvider props:children` | 8,141 |

## copilot-switch-threads · baseline — median of 3 runs

| metric | median | range | spread |
| --- | --- | --- | --- |
| commits | 288 | 286 – 310 | 8.3% |
| total renders | 269,370 | 261,379 – 295,570 | 12.7% |
| renders per commit | 935.3 | 913.9 – 953.5 | 4.2% |
| max renders in one commit | 13,364 | 13,364 – 13,364 | 0.0% |
| script time (ms) | 11,714 | 11,169 – 11,978 | 6.9% |
| task time (ms) | 13,451 | 12,973 – 13,686 | 5.3% |
| layout time (ms) | 143 | 140 – 148 | 5.8% |
| style recalc time (ms) | 376 | 355 – 381 | 6.8% |
| long tasks (≥ 50 ms) | 26 | 23 – 27 | 15.4% |
| long task total (ms) | 9,939 | 9,240 – 10,108 | 8.7% |
| heap used after GC (MB) | 90.4 | 90.4 – 92.6 | 2.4% |

**Top components (median renders)**

| name | renders |
| --- | --- |
| `Tooltip` | 19,082 |
| `TooltipContent` | 19,024 |
| `HugeiconsIcon` | 10,827 |
| `Icon` | 10,827 |
| `Presence` | 10,554 |
| `Primitive.div` | 10,049 |
| `Popper` | 9,955 |
| `PopperAnchor` | 9,955 |
| `PopperProvider` | 9,955 |
| `Primitive.div.Slot` | 9,955 |

**Top render causes (median renders)**

| name | renders |
| --- | --- |
| `Tooltip props:children` | 13,934 |
| `HugeiconsIcon parent-same-props<-Icon` | 8,920 |
| `Presence props:children` | 8,763 |
| `Primitive.div props:children` | 8,265 |
| `Primitive.div.Slot props:children` | 8,218 |
| `Primitive.div.SlotClone props:children` | 8,218 |
| `PopperAnchor props:children` | 8,095 |
| `Primitive.button props:children,ref,onPointerMove,onPointerLeave,+4` | 7,962 |
| `Primitive.button.Slot props:children,onPointerMove,onPointerLeave,onPointerDown,+4` | 7,962 |
| `Primitive.button.SlotClone props:onPointerMove,onPointerLeave,onPointerDown,onFocus,+4` | 7,962 |

## copilot-type · baseline — median of 3 runs

| metric | median | range | spread |
| --- | --- | --- | --- |
| commits | 38 | 38 – 38 | 0.0% |
| total renders | 2,869 | 2,869 – 2,869 | 0.0% |
| renders per commit | 75.5 | 75.5 – 75.5 | 0.0% |
| max renders in one commit | 151 | 151 – 151 | 0.0% |
| script time (ms) | 302 | 298 – 331 | 11.0% |
| task time (ms) | 490 | 488 – 523 | 7.2% |
| layout time (ms) | 34 | 32 – 34 | 5.4% |
| style recalc time (ms) | 27 | 26 – 28 | 7.0% |
| long tasks (≥ 50 ms) | 0 | 0 – 0 | 0.0% |
| long task total (ms) | 0 | 0 – 0 | 0.0% |
| heap used after GC (MB) | 85.3 | 84.3 – 85.4 | 1.3% |

**Top components (median renders)**

| name | renders |
| --- | --- |
| `Presence` | 171 |
| `Popper` | 133 |
| `PopperAnchor` | 133 |
| `PopperProvider` | 133 |
| `Primitive.div` | 133 |
| `Primitive.div.Slot` | 133 |
| `Primitive.div.SlotClone` | 133 |
| `Tooltip` | 114 |
| `TooltipContent` | 114 |
| `Primitive.button` | 95 |

**Top render causes (median renders)**

| name | renders |
| --- | --- |
| `Presence props:children` | 171 |
| `Primitive.div props:children` | 133 |
| `Primitive.div.Slot props:children` | 133 |
| `Primitive.div.SlotClone props:children` | 133 |
| `Popper props:children` | 114 |
| `PopperAnchor props:children` | 114 |
| `PopperProvider props:children` | 114 |
| `Tooltip props:children` | 114 |
| `HugeiconsIcon parent-same-props<-Icon` | 76 |
| `Icon parent-same-props<-Primitive.button.SlotClone` | 57 |

## library-running · baseline — median of 3 runs

| metric | median | range | spread |
| --- | --- | --- | --- |
| commits | 30 | 30 – 30 | 0.0% |
| total renders | 516 | 516 – 516 | 0.0% |
| renders per commit | 17.2 | 17.2 – 17.2 | 0.0% |
| max renders in one commit | 100 | 100 – 100 | 0.0% |
| script time (ms) | 69 | 64 – 75 | 14.9% |
| task time (ms) | 496 | 496 – 575 | 16.0% |
| layout time (ms) | 0 | 0 – 0 | 0.0% |
| style recalc time (ms) | 91 | 91 – 91 | 0.3% |
| long tasks (≥ 50 ms) | 0 | 0 – 0 | 0.0% |
| long task total (ms) | 0 | 0 – 0 | 0.0% |
| heap used after GC (MB) | 47.1 | 47.0 – 47.2 | 0.6% |

**Top components (median renders)**

| name | renders |
| --- | --- |
| `u0` | 60 |
| `Presence` | 20 |
| `on` | 18 |
| `oo` | 18 |
| `Primitive.div` | 15 |
| `da` | 12 |
| `em` | 12 |
| `oC` | 12 |
| `Primitive.button` | 12 |
| `r$` | 12 |

**Top render causes (median renders)**

| name | renders |
| --- | --- |
| `u0 context` | 60 |
| `Presence props:children` | 20 |
| `da state#0` | 12 |
| `em props:children` | 12 |
| `oC parent-same-props<-r$` | 12 |
| `on context` | 12 |
| `oo props:isDevBuilding` | 12 |
| `Primitive.div props:children` | 12 |
| `r$ props:state,children` | 12 |
| `rV props:state,children` | 12 |

## builder-connect-edge · baseline — median of 10 runs

Added later than the other tables: app code is `dev` at `a1578ad9a` (2026-10-19); build, browser and machine are the same as above. Ten saved runs instead of three. A few runs have extra commits in the window; total renders hardly move.

| metric | median | range | spread |
| --- | --- | --- | --- |
| commits | 20 | 20 – 36 | 80.0% |
| total renders | 9,197 | 9,197 – 9,457 | 2.8% |
| renders per commit | 459.9 | 262.7 – 459.9 | 42.9% |
| max renders in one commit | 8,682 | 8,682 – 8,682 | 0.0% |
| script time (ms) | 516 | 489 – 633 | 27.9% |
| task time (ms) | 673 | 641 – 863 | 33.0% |
| layout time (ms) | 3 | 2 – 4 | 48.4% |
| style recalc time (ms) | 7 | 6 – 10 | 59.1% |
| long tasks (≥ 50 ms) | 1 | 1 – 2 | 100.0% |
| long task total (ms) | 412 | 396 – 571 | 42.6% |
| heap used after GC (MB) | 85.4 | 84.0 – 85.8 | 2.0% |

**Top components (median renders)**

| name | renders |
| --- | --- |
| `Text` | 491 |
| `HugeiconsIcon` | 417 |
| `Icon` | 417 |
| `Primitive.div` | 401 |
| `Tooltip` | 370 |
| `TooltipContent` | 370 |
| `HandleComponent` | 346 |
| `SelectItem` | 296 |
| `TooltipProvider` | 295 |
| `Presence` | 240 |

**Top render causes (median renders)**

| name | renders |
| --- | --- |
| `Tooltip props:children` | 370 |
| `HugeiconsIcon parent-same-props<-Icon` | 362 |
| `HandleComponent state#8` | 344 |
| `SelectItem props:onMouseDown,children` | 296 |
| `TooltipProvider props:children` | 295 |
| `Presence props:children` | 240 |
| `Primitive.div props:children` | 231 |
| `Primitive.div.Slot props:children` | 231 |
| `Primitive.div.SlotClone props:children` | 231 |
| `Text props:children` | 224 |

## library-running-events · baseline — median of 3 runs

Added after the table above, so it was measured on `dev` at `a1578ad9a` (2026-10-19), on the same machine. The websocket is mocked; each run prints what it saw, and all three printed the same: 80 events in 20 s, counted until 2 s after the last one, caused 1,600 `subscribe_graph_executions` frames (all 20 workflows on every event) and 80 `GET /executions` refetches. Each refetch re-renders the whole card grid.

| metric | median | range | spread |
| --- | --- | --- | --- |
| commits | 242 | 242 – 242 | 0.0% |
| total renders | 143,052 | 143,052 – 143,052 | 0.0% |
| renders per commit | 591.1 | 591.1 – 591.1 | 0.0% |
| max renders in one commit | 1,768 | 1,768 – 1,768 | 0.0% |
| script time (ms) | 8,442 | 8,191 – 8,797 | 7.2% |
| task time (ms) | 10,585 | 10,338 – 10,975 | 6.0% |
| layout time (ms) | 55 | 55 – 58 | 6.5% |
| style recalc time (ms) | 160 | 139 – 175 | 22.7% |
| long tasks (≥ 50 ms) | 80 | 80 – 80 | 0.0% |
| long task total (ms) | 8,288 | 8,053 – 8,635 | 7.0% |
| heap used after GC (MB) | 43.5 | 43.5 – 43.5 | 0.1% |

**Top components (median renders)**

| name | renders |
| --- | --- |
| `HugeiconsIcon` | 9,760 |
| `Icon` | 9,760 |
| `Presence` | 8,642 |
| `Dialog` | 6,881 |
| `Text` | 5,440 |
| `motion.div` | 4,880 |
| `DialogPortal` | 3,441 |
| `DialogPortalProvider` | 3,441 |
| `DialogProvider` | 3,441 |
| `BaseContent` | 3,440 |

**Top render causes (median renders)**

| name | renders |
| --- | --- |
| `HugeiconsIcon parent-same-props<-Icon` | 9,760 |
| `Presence props:children` | 8,642 |
| `Icon parent-same-props<-motion.div` | 4,800 |
| `Dialog props:onOpenChange,children` | 3,441 |
| `DialogPortal props:children` | 3,441 |
| `DialogPortalProvider props:children` | 3,441 |
| `DialogProvider props:children` | 3,441 |
| `BaseContent props:children` | 3,440 |
| `DialogWrap props:styling,handleOpen,handleClose,children` | 3,440 |
| `LinkComponent props:children` | 3,200 |

## builder-run-events · baseline — median of 3 runs

Added with SECRT-3003: measured on `dev` app code at `a1578ad9a` on a similar 16 GB cloud sandbox.

| metric | median | range | spread |
| --- | --- | --- | --- |
| commits | 158 | 157 – 159 | 1.3% |
| total renders | 58,440 | 58,438 – 58,440 | 0.0% |
| renders per commit | 369.9 | 367.5 – 372.2 | 1.3% |
| max renders in one commit | 10,628 | 10,628 – 10,628 | 0.0% |
| script time (ms) | 2,528 | 2,367 – 2,774 | 16.1% |
| task time (ms) | 3,038 | 2,766 – 3,207 | 14.5% |
| layout time (ms) | 17 | 15 – 17 | 7.6% |
| style recalc time (ms) | 27 | 22 – 28 | 19.4% |
| long tasks (≥ 50 ms) | 6 | 6 – 10 | 66.7% |
| long task total (ms) | 1,868 | 1,588 – 2,065 | 25.5% |
| heap used after GC (MB) | 107.9 | 107.4 – 110.7 | 3.1% |

**Top components (median renders)**

| name | renders |
| --- | --- |
| `Presence` | 3,032 |
| `Text` | 2,667 |
| `HugeiconsIcon` | 2,438 |
| `Icon` | 2,438 |
| `Tooltip` | 2,266 |
| `TooltipContent` | 2,266 |
| `TooltipProvider` | 2,214 |
| `Primitive.div` | 1,962 |
| `Primitive.button` | 1,636 |
| `Popper` | 1,605 |

**Top render causes (median renders)**

| name | renders |
| --- | --- |
| `Presence props:children` | 2,932 |
| `HugeiconsIcon parent-same-props<-Icon` | 2,398 |
| `Tooltip props:children` | 2,226 |
| `TooltipProvider props:children` | 2,174 |
| `Primitive.div props:children` | 1,554 |
| `Primitive.div.Slot props:children` | 1,375 |
| `Primitive.div.SlotClone props:children` | 1,375 |
| `TooltipContent props:children` | 1,344 |
| `Primitive.button.Slot props:children,onPointerMove,onPointerLeave,onPointerDown,+4` | 1,217 |
| `Primitive.button.SlotClone props:onPointerMove,onPointerLeave,onPointerDown,onFocus,+4` | 1,217 |

## copilot-stream-code · baseline — median of 5 runs

Added after the table above was saved: `dev` at `a1578ad9a`, same machine type, 1 warm-up and 5 saved runs. The reply is one 130-line code block, so most of the time is the transcript re-rendering on every streamed word, plus Shiki highlighting the open block on each flush.

| metric | median | range | spread |
| --- | --- | --- | --- |
| commits | 2,573 | 2,565 – 2,588 | 0.9% |
| total renders | 579,560 | 576,640 – 580,000 | 0.6% |
| renders per commit | 224.5 | 224.1 – 226.1 | 0.9% |
| max renders in one commit | 1,010 | 1,010 – 1,010 | 0.0% |
| script time (ms) | 46,940 | 46,115 – 50,794 | 10.0% |
| task time (ms) | 55,268 | 54,101 – 58,986 | 8.8% |
| layout time (ms) | 461 | 428 – 486 | 12.8% |
| style recalc time (ms) | 1,616 | 1,523 – 1,690 | 10.3% |
| long tasks (≥ 50 ms) | 377 | 350 – 407 | 15.1% |
| long task total (ms) | 33,039 | 31,008 – 37,272 | 19.0% |
| heap used after GC (MB) | 70.8 | 70.4 – 73.5 | 4.3% |

**Top components (median renders)**

| name | renders |
| --- | --- |
| `Tooltip` | 35,872 |
| `TooltipContent` | 35,871 |
| `Presence` | 24,642 |
| `Popper` | 20,406 |
| `PopperAnchor` | 20,406 |
| `PopperProvider` | 20,406 |
| `Primitive.button` | 20,406 |
| `Primitive.button.Slot` | 20,406 |
| `Primitive.button.SlotClone` | 20,406 |
| `Primitive.div` | 20,406 |

**Top render causes (median renders)**

| name | renders |
| --- | --- |
| `Tooltip props:children` | 34,456 |
| `Presence props:children` | 24,631 |
| `Primitive.div props:children` | 20,397 |
| `Primitive.div.Slot props:children` | 20,397 |
| `Primitive.div.SlotClone props:children` | 20,397 |
| `TooltipProvider props:children` | 20,388 |
| `HugeiconsIcon parent-same-props<-Icon` | 19,744 |
| `PopperAnchor props:children` | 19,676 |
| `Popper props:children` | 19,667 |
| `PopperProvider props:children` | 19,667 |

## copilot-tool-wait · baseline — median of 3 runs

Added with SECRT-2995 and measured on `dev` at `a1578ad9a`, before that change. Each of the 20 long tasks is one tick of the thinking timer, which re-rendered the whole transcript once a second while the tool ran.

| metric | median | range | spread |
| --- | --- | --- | --- |
| commits | 45 | 45 – 49 | 8.9% |
| total renders | 183,109 | 183,109 – 183,145 | 0.0% |
| renders per commit | 4,069.1 | 3,737.7 – 4,069.1 | 8.1% |
| max renders in one commit | 9,134 | 9,134 – 9,134 | 0.0% |
| script time (ms) | 5,475 | 5,448 – 5,719 | 4.9% |
| task time (ms) | 7,248 | 7,058 – 7,546 | 6.7% |
| layout time (ms) | 0 | 0 – 0 | 0.0% |
| style recalc time (ms) | 190 | 168 – 196 | 14.2% |
| long tasks (≥ 50 ms) | 20 | 20 – 20 | 0.0% |
| long task total (ms) | 5,439 | 5,412 – 5,693 | 5.2% |
| heap used after GC (MB) | 91.8 | 89.2 – 96.6 | 8.0% |

**Top components (median renders)**

| name | renders |
| --- | --- |
| `Tooltip` | 13,532 |
| `TooltipContent` | 13,532 |
| `HugeiconsIcon` | 7,754 |
| `Icon` | 7,754 |
| `TooltipProvider` | 6,786 |
| `Presence` | 6,780 |
| `Popper` | 6,776 |
| `PopperAnchor` | 6,776 |
| `PopperProvider` | 6,776 |
| `Primitive.button` | 6,776 |

**Top render causes (median renders)**

| name | renders |
| --- | --- |
| `Tooltip props:children` | 13,532 |
| `HugeiconsIcon parent-same-props<-Icon` | 7,754 |
| `TooltipProvider props:children` | 6,786 |
| `Presence props:children` | 6,780 |
| `Primitive.div props:children` | 6,776 |
| `Primitive.div.Slot props:children` | 6,776 |
| `Primitive.div.SlotClone props:children` | 6,776 |
| `Popper props:children` | 6,770 |
| `PopperAnchor props:children` | 6,770 |
| `PopperProvider props:children` | 6,770 |

## copilot-resize-drag · baseline — median of 3 runs

Added with the scenario (SECRT-2999): `dev` at `6d793e53d`, same machine type, 1 warm-up and 3 saved runs. Every pointer move of the drag wrote the panel width to the copilot UI store, and the page hook read the whole store, so each move re-rendered the whole 200-message transcript: about 3,000 renders per commit and one long task per move.

| metric | median | range | spread |
| --- | --- | --- | --- |
| commits | 193 | 190 – 196 | 3.1% |
| total renders | 574,244 | 574,001 – 574,251 | 0.0% |
| renders per commit | 2,975.4 | 2,929.9 – 3,021.1 | 3.1% |
| max renders in one commit | 9,498 | 9,498 – 9,498 | 0.0% |
| script time (ms) | 17,819 | 17,358 – 18,387 | 5.8% |
| task time (ms) | 20,508 | 19,966 – 20,952 | 4.8% |
| layout time (ms) | 575 | 575 – 577 | 0.4% |
| style recalc time (ms) | 34 | 34 – 35 | 2.1% |
| long tasks (≥ 50 ms) | 60 | 60 – 60 | 0.0% |
| long task total (ms) | 19,661 | 19,213 – 20,218 | 5.1% |
| heap used after GC (MB) | 86.5 | 84.1 – 95.6 | 13.4% |

**Top components (median renders)**

| name | renders |
| --- | --- |
| `Tooltip` | 41,418 |
| `TooltipContent` | 41,418 |
| `HugeiconsIcon` | 24,264 |
| `Icon` | 24,264 |
| `Presence` | 22,172 |
| `Popper` | 21,204 |
| `PopperAnchor` | 21,204 |
| `PopperProvider` | 21,204 |
| `Primitive.button` | 21,204 |
| `Primitive.button.Slot` | 21,204 |

**Top render causes (median renders)**

| name | renders |
| --- | --- |
| `Tooltip props:children` | 41,298 |
| `HugeiconsIcon parent-same-props<-Icon` | 24,264 |
| `Presence props:children` | 22,172 |
| `Primitive.div props:children` | 21,204 |
| `Primitive.div.Slot props:children` | 21,204 |
| `Primitive.div.SlotClone props:children` | 21,204 |
| `Popper props:children` | 20,955 |
| `PopperAnchor props:children` | 20,955 |
| `PopperProvider props:children` | 20,955 |
| `TooltipProvider props:children` | 20,949 |

## builder-run-beads · baseline — median of 5 runs

Added with SECRT-3009: `dev` app code at `6d793e53d` on a similar 16 GB cloud sandbox, 2 warm-ups and 5 saved runs. On this code every bead re-renders `JSBeads` once per animation frame (about 150 `JSBeads state#0` renders in the window); the heavy commits are the nodes re-rendering on each new execution.


| metric | median | range | spread |
| --- | --- | --- | --- |
| commits | 84 | 79 – 85 | 7.1% |
| total renders | 54,037 | 53,947 – 54,078 | 0.2% |
| renders per commit | 642.7 | 636.2 – 684.0 | 7.4% |
| max renders in one commit | 7,285 | 7,285 – 7,285 | 0.0% |
| script time (ms) | 2,332 | 2,142 – 2,505 | 15.6% |
| task time (ms) | 2,791 | 2,567 – 3,057 | 17.6% |
| layout time (ms) | 39 | 38 – 43 | 13.0% |
| style recalc time (ms) | 31 | 29 – 32 | 10.9% |
| long tasks (≥ 50 ms) | 16 | 11 – 17 | 37.5% |
| long task total (ms) | 2,274 | 2,069 – 2,613 | 23.9% |
| heap used after GC (MB) | 99.4 | 97.1 – 103.1 | 6.0% |

**Top components (median renders)**

| name | renders |
| --- | --- |
| `Text` | 2,954 |
| `TooltipProvider` | 2,819 |
| `HugeiconsIcon` | 2,675 |
| `Icon` | 2,675 |
| `Primitive.div` | 2,411 |
| `Tooltip` | 2,322 |
| `TooltipContent` | 2,322 |
| `Presence` | 1,561 |
| `SelectItem` | 1,512 |
| `Primitive.div.Slot` | 1,427 |

**Top render causes (median renders)**

| name | renders |
| --- | --- |
| `TooltipProvider props:children` | 2,759 |
| `HugeiconsIcon parent-same-props<-Icon` | 2,323 |
| `Tooltip props:children` | 2,282 |
| `SelectItem props:onMouseDown,children` | 1,512 |
| `Presence props:children` | 1,461 |
| `Primitive.div props:children` | 1,437 |
| `Primitive.div.Slot props:children` | 1,407 |
| `Primitive.div.SlotClone props:children` | 1,407 |
| `Popper props:children` | 1,269 |
| `PopperAnchor props:children` | 1,269 |

## library-agent-runs · baseline — median of 3 runs

Added with SECRT-3010 and measured on `dev` at `ede2df2ae`, before that change. The websocket is mocked; every run printed the same: 3 new runs caused 3 `GET` refetches of the run list and the selected RUNNING run polled its detail 5 times. Each new run re-rendered every row of the list about twice (20 to 23 cards, each with its actions menu and two dialogs). Total renders move by one full-list re-render (about 1,300 renders) from run to run.

| metric | median | range | spread |
| --- | --- | --- | --- |
| commits | 40 | 38 – 41 | 7.5% |
| total renders | 9,722 | 8,462 – 9,722 | 13.0% |
| renders per commit | 237.1 | 222.7 – 243.1 | 8.6% |
| max renders in one commit | 1,252 | 1,252 – 1,260 | 0.6% |
| script time (ms) | 785 | 720 – 789 | 8.7% |
| task time (ms) | 978 | 918 – 999 | 8.2% |
| layout time (ms) | 9 | 8 – 10 | 24.7% |
| style recalc time (ms) | 13 | 12 – 13 | 8.9% |
| long tasks (≥ 50 ms) | 6 | 6 – 6 | 0.0% |
| long task total (ms) | 579 | 539 – 588 | 8.5% |
| heap used after GC (MB) | 43.3 | 43.2 – 44.0 | 1.8% |

**Top components (median renders)**

| name | renders |
| --- | --- |
| `Presence` | 926 |
| `Dialog` | 687 |
| `Text` | 390 |
| `DialogPortal` | 344 |
| `DialogPortalProvider` | 344 |
| `DialogProvider` | 344 |
| `BaseContent` | 343 |
| `DialogWrap` | 343 |
| `MenuProvider` | 302 |
| `Primitive.div` | 259 |

**Top render causes (median renders)**

| name | renders |
| --- | --- |
| `Presence props:children` | 926 |
| `Dialog props:onOpenChange,children` | 344 |
| `DialogPortal props:children` | 344 |
| `DialogPortalProvider props:children` | 344 |
| `DialogProvider props:children` | 344 |
| `BaseContent props:children` | 343 |
| `DialogWrap props:styling,handleOpen,handleClose,children` | 343 |
| `Dialog props:controlled,styling,children` | 328 |
| `MenuProvider props:scope,children` | 302 |
| `Primitive.div props:children` | 224 |

## builder-type-field · baseline — median of 3 runs

Added with SECRT-3012: `dev` app code at `8eb406275` on a similar 16 GB cloud sandbox, Chrome 141 (`--channel chrome`), 1 warm-up and 3 saved runs. On this code every keystroke re-renders `<Flow>` and the panels beside the canvas (`BuilderChatPanel`, `FloatingReviewsPanel`, `DraftRecoveryPopup`, `FloatingSafeModeToggle`): 20 renders each.

| metric | median | range | spread |
| --- | --- | --- | --- |
| commits | 120 | 120 – 120 | 0.0% |
| total renders | 9,680 | 9,680 – 9,680 | 0.0% |
| renders per commit | 80.7 | 80.7 – 80.7 | 0.0% |
| max renders in one commit | 207 | 207 – 207 | 0.0% |
| script time (ms) | 826 | 702 – 929 | 27.5% |
| task time (ms) | 1,013 | 862 – 1,114 | 24.9% |
| layout time (ms) | 16 | 14 – 17 | 16.1% |
| style recalc time (ms) | 2 | 2 – 2 | 10.0% |
| long tasks (≥ 50 ms) | 4 | 0 – 7 | 175.0% |
| long task total (ms) | 257 | 0 – 500 | 194.2% |
| heap used after GC (MB) | 82.1 | 82.0 – 82.1 | 0.2% |

**Top components (median renders)**

| name | renders |
| --- | --- |
| `Primitive.div` | 440 |
| `Tooltip` | 360 |
| `TooltipContent` | 360 |
| `TooltipProvider` | 340 |
| `Presence` | 320 |
| `HugeiconsIcon` | 300 |
| `Icon` | 300 |
| `SelectItem` | 280 |
| `Popper` | 260 |
| `PopperAnchor` | 260 |

**Top render causes (median renders)**

| name | renders |
| --- | --- |
| `Tooltip props:children` | 360 |
| `TooltipProvider props:children` | 340 |
| `Presence props:children` | 320 |
| `HugeiconsIcon parent-same-props<-Icon` | 280 |
| `SelectItem props:onMouseDown,children` | 280 |
| `Primitive.div props:children` | 260 |
| `Primitive.div.Slot props:children` | 260 |
| `Primitive.div.SlotClone props:children` | 260 |
| `Popper props:children` | 220 |
| `PopperAnchor props:children` | 220 |

## builder-block-menu-search · baseline — median of 3 runs

Added with SECRT-3012: `dev` app code at `8eb406275`, same machine and settings. Results come from the real backend's block search, so the counts depend on the blocks the backend has. On this code each new page of results re-renders every `Block` row already in the list (`Block parent-same-props<-InfiniteScroll`). One run in three loaded one page fewer within the window (1,461 renders).

| metric | median | range | spread |
| --- | --- | --- | --- |
| commits | 64 | 59 – 64 | 7.8% |
| total renders | 1,843 | 1,461 – 1,843 | 20.7% |
| renders per commit | 28.8 | 24.8 – 28.8 | 14.0% |
| max renders in one commit | 211 | 179 – 211 | 15.2% |
| script time (ms) | 268 | 195 – 274 | 29.4% |
| task time (ms) | 937 | 810 – 992 | 19.5% |
| layout time (ms) | 11 | 9 – 13 | 28.8% |
| style recalc time (ms) | 41 | 36 – 41 | 13.3% |
| long tasks (≥ 50 ms) | 0 | 0 – 1 | 0.0% |
| long task total (ms) | 0 | 0 – 78 | 0.0% |
| heap used after GC (MB) | 84.1 | 83.6 – 85.3 | 2.0% |

**Top components (median renders)**

| name | renders |
| --- | --- |
| `Button` | 302 |
| `HugeiconsIcon` | 292 |
| `Icon` | 292 |
| `Block` | 256 |
| `u0` | 120 |
| `AnimatePresence` | 42 |
| `FilterChip` | 35 |
| `PopChild` | 35 |
| `PopChildMeasure` | 35 |
| `PresenceChild` | 35 |

**Top render causes (median renders)**

| name | renders |
| --- | --- |
| `HugeiconsIcon parent-same-props<-Icon` | 239 |
| `Icon parent-same-props<-Button` | 218 |
| `Block parent-same-props<-InfiniteScroll` | 208 |
| `Button props:onDragStart,onClick,children` | 208 |
| `u0 context` | 120 |
| `Button mount` | 54 |
| `HugeiconsIcon mount` | 53 |
| `Icon mount` | 53 |
| `Block mount` | 48 |
| `AnimatePresence state#5` | 30 |

## builder-load-graph · baseline — median of 3 runs

Added with SECRT-3012: `dev` app code at `8eb406275`, same machine and settings. The window opens on the 1-node workflow and closes 2 s after the last of the 100 nodes is on the canvas. On this code the switch also shows the "Unsaved changes found" popup for the new workflow.

| metric | median | range | spread |
| --- | --- | --- | --- |
| commits | 71 | 71 – 74 | 4.2% |
| total renders | 80,164 | 80,164 – 80,193 | 0.0% |
| renders per commit | 1,129.1 | 1,083.7 – 1,129.1 | 4.0% |
| max renders in one commit | 25,160 | 25,160 – 25,160 | 0.0% |
| script time (ms) | 3,100 | 3,022 – 3,244 | 7.2% |
| task time (ms) | 3,706 | 3,616 – 3,881 | 7.1% |
| layout time (ms) | 51 | 48 – 52 | 7.8% |
| style recalc time (ms) | 111 | 107 – 111 | 3.4% |
| long tasks (≥ 50 ms) | 4 | 4 – 4 | 0.0% |
| long task total (ms) | 3,062 | 2,989 – 3,202 | 7.0% |
| heap used after GC (MB) | 105.0 | 104.8 – 108.1 | 3.1% |

**Top components (median renders)**

| name | renders |
| --- | --- |
| `TooltipProvider` | 3,700 |
| `Text` | 3,370 |
| `Tooltip` | 3,103 |
| `TooltipContent` | 3,103 |
| `Primitive.div` | 3,075 |
| `HugeiconsIcon` | 3,017 |
| `Icon` | 3,017 |
| `Presence` | 2,825 |
| `Popper` | 2,823 |
| `PopperProvider` | 2,823 |

**Top render causes (median renders)**

| name | renders |
| --- | --- |
| `TooltipProvider props:children` | 2,120 |
| `Presence props:children` | 2,044 |
| `HugeiconsIcon parent-same-props<-Icon` | 1,706 |
| `Primitive.div props:children` | 1,663 |
| `Primitive.div.Slot props:children` | 1,663 |
| `Primitive.div.SlotClone props:children` | 1,663 |
| `Tooltip props:children` | 1,660 |
| `PopperAnchor props:children` | 1,417 |
| `Text mount` | 1,260 |
| `Text props:children` | 1,154 |

## copilot-open-long-thread · baseline — median of 3 runs

Measured on `dev` at `a7fe56d05` (2026-10-20), before the transcript was windowed (SECRT-3011), with `--channel chrome`. Opening the 2,000-message chat mounts 924 rows in one commit; the longest task is about 2.5 s.

| metric | median | range | spread |
| --- | --- | --- | --- |
| commits | 27 | 27 – 28 | 3.7% |
| total renders | 59,027 | 59,025 – 59,042 | 0.0% |
| renders per commit | 2,186.1 | 2,108.6 – 2,186.2 | 3.5% |
| max renders in one commit | 38,844 | 38,844 – 38,844 | 0.0% |
| script time (ms) | 2,546 | 2,537 – 2,708 | 6.7% |
| task time (ms) | 3,337 | 3,240 – 3,387 | 4.4% |
| layout time (ms) | 72 | 71 – 80 | 12.5% |
| style recalc time (ms) | 209 | 194 – 230 | 16.9% |
| long tasks (≥ 50 ms) | 8 | 8 – 8 | 0.0% |
| long task total (ms) | 3,007 | 2,986 – 3,155 | 5.6% |
| heap used after GC (MB) | 119.8 | 119.7 – 119.8 | 0.1% |

**Top components (median renders)**

| name | renders |
| --- | --- |
| `Tooltip` | 3,337 |
| `TooltipContent` | 3,337 |
| `Streamdown` | 2,772 |
| `Presence` | 2,303 |
| `TooltipProvider` | 2,238 |
| `Popper` | 2,235 |
| `PopperAnchor` | 2,235 |
| `PopperProvider` | 2,235 |
| `Primitive.button` | 2,235 |
| `Primitive.button.Slot` | 2,235 |

**Top render causes (median renders)**

| name | renders |
| --- | --- |
| `Tooltip mount` | 2,194 |
| `TooltipContent mount` | 2,194 |
| `Streamdown state#1` | 1,848 |
| `Presence props:children` | 1,196 |
| `Block mount` | 1,177 |
| `Ie mount` | 1,177 |
| `HugeiconsIcon mount` | 1,146 |
| `Icon mount` | 1,146 |
| `Primitive.div props:children` | 1,136 |
| `Primitive.div.Slot props:children` | 1,136 |

---

### Engineering Frontend Perf Measurement (`docs/engineering/frontend-perf-measurement.md`)

# Frontend performance measurement

How to show that a frontend change made the app faster or slower: run a fixed scenario before and after the change and compare the medians. The tooling lives in `autogpt_platform/frontend` and only ever runs on a developer machine. It is not part of CI, and nothing it adds reaches a production bundle.

Saved baseline tables for the current scenarios: [frontend-perf-baseline.md](frontend-perf-baseline.md).

## The three numbers

- **Re-renders.** React draws the screen from small components (a chat row, a button, a sidebar entry). When data changes, React redraws the affected components; each redraw is a *render*, and one batch of them is a *commit*. Code often redraws far more than it needs to, for example every row of a 200-message chat when only the streaming row changed. Every wasted render costs CPU time.
- **JS CPU.** The milliseconds the browser's main thread spends running our JavaScript. The main thread also handles clicks, layout and painting, so 80 ms of our code in one go freezes the page for 80 ms. A task of 50 ms or more is a *long task*, which is what people feel as jank: stuttering, typing lag, jerky scrolling.
- **JS heap.** The memory our JavaScript holds after garbage collection. A heap that grows and never shrinks slows the tab down and eventually crashes it. It grows when code keeps references it no longer needs, such as a finished chat session that is never dropped.

They are one problem seen from three sides: wasted renders burn CPU, CPU burns the 16 ms frame budget, and leftover renders hold memory.

## What is there

| Piece | Where | What it does |
| --- | --- | --- |
| Render counter | `public/devtools/render-counter.js` | Hooks into React and, after every commit, records which components rendered and why. Loaded only when `NEXT_PUBLIC_RENDER_COUNTER=1`. |
| Measurement script | `scripts/perf/measure.ts` (`pnpm perf`) | Opens the app in Chromium, runs one scenario, and reads the counter plus Chrome DevTools Protocol metrics. Writes one JSON per run. |
| Compare script | `scripts/perf/compare.ts` (`pnpm perf:compare`) | Prints the medians of two labelled sets side by side as markdown. |
| Scenarios | `scripts/perf/scenarios/*.ts` | One file per scenario: setup, the measured interaction, and the window length. |
| Fixtures | `scripts/perf/fixtures/`, `scripts/perf/lib/*-fixture.ts` | Recorded or generated API responses served with `page.route`, so every run sees the same data and the same streamed reply. |
| Intercepted URLs | `scripts/perf/lib/routes.ts` | The one list of URLs the harness answers itself. Everything else goes to the real dev server and backend. |

## Run one scenario

1. Start the backend as usual (Postgres for Better Auth, plus `rest_server` and `websocket_server`). Copilot, builder and library data come from fixtures, so the executors, ClamAV and the LLM are not needed. On a 16 GB machine, stop `clamav`, `executor`, `copilot_executor`, `scheduler_server` and `notification_server`: the dev server needs about 5 GB and gets killed for memory otherwise.
2. Start the frontend with the counter. `NEXT_PUBLIC_*` values are read when the dev server starts, so restart it after changing the flag:

   ```bash
   cd autogpt_platform/frontend
   NEXT_PUBLIC_RENDER_COUNTER=1 pnpm dev
   ```

3. Install Playwright's Chromium once (`npx playwright install chromium`), or pass `--channel chrome` to use an installed Chrome.
4. In another terminal:

   ```bash
   pnpm perf --list                                            # scenarios
   pnpm perf --scenario copilot-stream-reply --label before    # 1 warm-up + 3 runs
   ```

Each run prints a table and saves `.perf/<scenario>/<label>-<timestamp>-<run>.json` (`.perf/` is gitignored). When there is more than one run, the script also prints the median, range and spread of each metric.

| Flag | Default | Meaning |
| --- | --- | --- |
| `--scenario <name>` | required | Which scenario to run. |
| `--label <text>` | `any` | Name of the set, e.g. `before`, `after`, `baseline`. Letters, digits, `.`, `_`, `-`. |
| `--runs <n>` | `3` | Saved runs. |
| `--warmup <n>` | `1` | Unsaved runs first, so a dev-server compile or a cold cache after a restart or a code edit does not land in a saved run. |
| `--base-url <url>` | `http://localhost:3000` | Frontend to measure. |
| `--profile` | off | Also save a `.cpuprofile` for each run. Open it in Chrome DevTools → Performance → Load profile. |
| `--screenshot` | off | Save a PNG of the page at the end of each window, to check the scenario did what it says. |
| `--headed` | off | Show the browser. |
| `--channel <name>` | Playwright Chromium | Browser channel, e.g. `chrome`. Also `PERF_BROWSER_CHANNEL`. |
| `--storage-state <file>` | none | Reuse a signed-in Playwright state, e.g. `.auth/states/smokeLibrary.json` from the e2e global setup. |

Without `--storage-state`, the script signs in through Better Auth as `perf.harness@example.com` and creates that account the first time. Set `PERF_USER_EMAIL` and `PERF_USER_PASSWORD` to use another account. It also marks onboarding and the first-visit tab intros as done, so no welcome card covers the page being measured.

If `window.__renderCounter` is missing, the script stops and tells you to restart the dev server with the flag.

## Before and after

1. Check out the commit **before** the change and run the scenario the change targets with `--label before`.
2. Check out the commit **with** the change and run it with `--label after`.
3. Compare, and paste the output in the PR description:

   ```bash
   pnpm perf:compare --a before --b after --scenario copilot-stream-reply
   ```

Leave out `--scenario` to compare every scenario that has runs under both labels.

When the change adds the scenario it is measured with (as `copilot-tool-wait` was), the commit before it has no such scenario. Stay on the branch, so `scripts/perf/` keeps the scenario and its fixture, and swap only the app files back to the base for the `before` runs:

```bash
git checkout origin/dev -- <app files the change touches>   # e.g. the files under src/
pnpm perf --scenario copilot-tool-wait --label before
git checkout HEAD -- <the same files>
pnpm perf --scenario copilot-tool-wait --label after
```

The dev server hot-reloads each swap, and the default warm-up run absorbs the recompile. Swap only files under `src/`, never `scripts/perf/`.

The output has three tables (metrics, top components and top render causes), each with `before`, `after` and `Δ` columns, followed by the raw runs in a collapsed block. Every value is the median of the runs, not the average.

Tips for numbers you can trust:

- Compare on the same machine and the same build type. Dev React is slower than production React, but a before/after delta on the same build type is still valid.
- Alternate the two checkouts when you can (before, after, before, after) so a machine that slows down over time does not favour one side.
- Keep the rest of the setup equal on both sides. `.env.default` turns on the React Query devtools and the token devtool, and they render too, so leave them as they are or change them for both runs.
- Read total renders and the per-component counts first: they are deterministic, and on the baseline most scenarios repeat them exactly from run to run. The exception is the end of a copilot turn, which now and then does one full-transcript re-render fewer (see the baseline). Commits, and so renders per commit, depend on how React batches updates into frames, so they can shift once a change makes rendering cheaper. Timing metrics move by a few percent. Treat any change smaller than the spread the script prints as noise.

## Reading the causes column

Each render is attributed to one cause, checked in this order:

| Cause | Meaning | Where to look |
| --- | --- | --- |
| `mount` | The component appeared this commit. | Expected on navigation; suspicious if it repeats while nothing appears (a changing `key`, or a parent that unmounts and remounts it). |
| `state#N` | Its own `useState`/`useReducer` (or a store read through `useSyncExternalStore`, such as zustand or React Query) changed. `N` is the hook's position in the component, counting every hook. | The Nth hook call in the component. |
| `props:a,b` | Its parent passed different values for these props (up to four, then `+N`). | Usually a new object, array or function created on every parent render, or an over-broad prop. |
| `parent-same-props<-Parent` | `Parent` re-rendered and passed props with identical values, but the component is not memoised. | Wrapping in `memo`, or moving state down so `Parent` stops re-rendering. |
| `context` | Nothing above changed, so a context it reads did. | A provider whose `value` is a new object every render, or a context that carries too much. |

A long run of `props:children` usually means the parent rebuilds its JSX every commit. Look for the first component in the list that is not a design-system wrapper (Radix `Presence`, `Popper` and `Slot` mostly pass through what they are given).

Names come from the component function. Third-party code bundled already minified shows short names such as `u0`; it still counts, but the name does not help.

## Scenarios

| Name | Setup | Measured | Window |
| --- | --- | --- | --- |
| `copilot-idle` | Open the 200-message chat | Nothing | 20 s |
| `copilot-stream-reply` | Open the 200-message chat | Type "go", press Enter, stream the 20-line fixture reply at about 33 chunks a second | Until the reply has been sent and the page has refetched the session, + 1 s |
| `copilot-background-stream` | Start the same reply slowly (200 ms a chunk) in the 200-message chat, click Home, and wait 3 s for Home's entrance animations | Nothing | 15 s |
| `copilot-type` | Open the 200-message chat and focus the composer | Type 19 characters, 50 ms apart | 2 s after the last key |
| `copilot-switch-threads` | Open the 200-message chat and list the 200 / 50 / 10-message chats in the sidebar | Click 50 → 10 → 200, three rounds, 2 s on each | Until done |
| `builder-drag-running` | Open a 50-node workflow whose run is RUNNING (the page polls it every second) | Drag one node for 2 s with 60 pointer moves | 2 s |
| `library-running` | Open the library with 20 workflows, all with a RUNNING run | Nothing | 30 s |
| `copilot-stream-code` | Open the 10-message chat | Type "go", press Enter, stream a reply that is one 130-line code block (`fixtures/reply-long-code.sse`) at about 33 chunks a second | Until the last line of the reply is on screen and the page has refetched the session, + 1 s |
| `builder-connect-edge` | Open the same 50-node workflow with no run (nothing polls) | Drag one new edge from StoreValue node 4's `output` to Calculator node 5's `a` input, 20 pointer moves over 0.6 s | 2 s after the drop |
| `library-running-events` | Open the same library at `/library?sort=updatedAt`, with the websocket mocked | Push a `graph_execution_event` for one of the 20 runs every 250 ms, 80 in all; print the `subscribe_graph_executions` frames and `GET /executions` requests seen | 20 s of events + 2 s |
| `builder-run-events` | Open the same 50-node workflow with its RUNNING run; the harness answers the websocket subscribe for that run itself | 40 node execution events over the websocket, one every 50 ms: the 10 unfinished nodes go RUNNING then COMPLETED, each event sent twice. The execution GET returns what has been sent so far, as the backend would | 2 s after the last event |
| `copilot-tool-wait` | Open the 200-message chat, send "go", and wait until the reply's one tool call shows "Running Perf lookup…". The reply then sends only `: heartbeat` comments, so the turn stays on that tool | Nothing | 20 s |
| `copilot-resize-drag` | Open the 200-message chat and click "Open files" so the files panel docks on the right | Drag the panel's resize handle 120 px wider with 60 pointer moves over 1 s | 1 s after the drop |
| `builder-run-beads` | Open the same 50-node workflow with its RUNNING run; the harness answers the websocket subscribe for that run itself | 10 new node executions over the websocket (a second, finished run of nodes 30 to 39 with new `node_exec_id`s), one every 150 ms, so each edge into one of those StoreValue nodes gets a new bead that travels the edge and is removed on arrival | 2 s after the last event |
| `library-agent-runs` | Open a workflow's page with 20 runs; the newest is RUNNING and selected, so its detail polls every 1.5 s. The websocket is mocked | Three new RUNNING runs arrive at the top of the list, 2.5 s apart: each is pushed as a `graph_execution_event`, the list refetches and the scenario waits for its row; prints the run-list and run-detail `GET`s seen | 1 s after the last run's 2.5 s |
| `builder-type-field` | Open the same 50-node workflow with no run, click the first node's text field and clear it | Type 20 characters, 50 ms apart | 2 s after the last key |
| `builder-block-menu-search` | Open the same 50-node workflow with no run and open the block menu | Type "text" into its search, 120 ms a key, wait for the first result, then scroll the results 6 times (800 px, 400 ms apart) so more pages load. Results come from the real backend's block search | 2 s after the last scroll |
| `builder-load-graph` | Open a 1-node workflow | Switch to a saved 100-node workflow with `history.pushState`, as opening another workflow does, until its last node is on the canvas | 2 s after the last node appears |
| `copilot-open-long-thread` | Open the 10-message chat and list the 2,000-message one in the sidebar. The 2,000 rows are the 200 recorded ones repeated, with fresh ids and sequences past 200 | Click the 2,000-message chat until its newest message is on screen | 2 s after that |

Each scenario's `readyWhen` field says exactly what setup waits for before the window opens. Most wait for the page's own content, then for network idle (at most 15 s, because some pages poll), then one more second.

Each run uses a fresh browser context at 1440 × 900, with the `en-US` locale and UTC time. Feature flags are pinned to their code defaults (`/api/feature-flags` answers `{flags: {}, configured: false}`), so a PostHog setup on one machine does not change what renders.

## What a run measures

1. Sign in, open the page, set up the scenario (`scenario.setup`).
2. Reset the counter, start a Chrome trace (`devtools.timeline`), optionally start the CPU profiler, read `Performance.getMetrics`, start the counter.
3. Run the interaction, then wait `windowMs`.
4. Stop the counter, read `Performance.getMetrics` again, stop the trace and the profiler.
5. Force a garbage collection (`HeapProfiler.collectGarbage`), wait 500 ms and read `JSHeapUsedSize`.

| Metric | Source |
| --- | --- |
| commits, total renders, renders per commit, max renders in one commit | render counter |
| top components, top render causes | render counter (the JSON keeps the top 50) |
| script, task, layout and style recalc time (ms) | `Performance.getMetrics` after minus before |
| long tasks (count and total ms) | trace `RunTask` events of 50 ms or more on the page's renderer main thread |
| heap used after GC (MB) | `JSHeapUsedSize` after a forced GC |

## Using the render counter by hand

With the dev server started with the flag, open the browser console on any page:

```js
__renderCounter.reset();
// …click around…
__renderCounter.print(); // console.table of the top components and causes
__renderCounter.report(); // { commits, totalRenders, rendersPerCommit, maxRendersPerCommit, components, causes }
```

`stop()` and `start()` pause and resume recording. It records from page load until you call `stop()`.

How it works: React reports every commit to `window.__REACT_DEVTOOLS_GLOBAL_HOOK__` if that object exists before React loads, which is why the script is loaded with `strategy="beforeInteractive"`. The script wraps an existing hook (the React DevTools extension, or Fast Refresh in dev) or installs a minimal one. On each commit it walks the fiber tree and counts function, class, `forwardRef` and simple `memo` components whose `PerformedWork` flag is set. It skips subtrees React bailed out of, because their fibers keep stale flags from older commits. `memo` wrappers that hold another component (tag 14) are not counted, because React flags both the wrapper and the component inside it and one render would count twice.

Production stays untouched: the file is in `public/` and is never bundled, and the `<Script>` tag in `src/app/layout.tsx` is only rendered when `NEXT_PUBLIC_RENDER_COUNTER` is exactly `"1"`. The root layout is a server component, so a production server reads the variable from its own environment at request time; no deployment sets it. After `pnpm build` without the flag, `grep -r __renderCounter .next/static` finds nothing.

## Fixtures

The copilot scenarios do not need an LLM:

- `fixtures/session-200.json`: 200 message rows (text, code blocks, tables, lists and tool calls). The 50- and 10-message chats are its first 50 and 10 rows.
- `fixtures/reply-20-lines.sse`: one streamed reply in the backend's SSE format, one word per `text-delta`.
- `fixtures/reply-long-code.sse`: a reply that is mostly one 130-line TypeScript block, streamed the same way, so nearly every chunk lands inside an open code fence.
- `lib/stream-server.ts`: a small local HTTP server that replays the `.sse` file one frame every N ms. The stream request is redirected there with `route.continue({ url })`, so the reply arrives at a steady, repeatable pace. The copilot's own word-by-word smoothing (`copilotStreamSmoothing.ts`) then runs on top, as it does in production.
- `lib/copilot-fixture.ts` keeps state: once a streamed turn ends, the session GET returns the new user and assistant rows, as the backend would. Without that, the page's refetch after each turn would wipe the reply it just streamed.

The builder and library fixtures (`lib/builder-fixture.ts`, `lib/library-fixture.ts`) are built in code: a 50-node workflow owned by the signed-in user with a RUNNING run (30 nodes done, 3 running, 7 queued), and 20 workflows each with a RUNNING run. Block schemas still come from the real backend.

**Regenerating the synthetic copilot fixtures** (deterministic, same output every time):

```bash
npx tsx scripts/perf/fixtures/generate.ts
```

**Re-recording from a real stack** when the wire format changes:

```bash
npx tsx scripts/perf/record.ts --session-id <id>                    # the chat's messages → session-200.json
npx tsx scripts/perf/record.ts --session-id <id> --message "go"     # …and one streamed reply → reply-20-lines.sse
```

The session is saved before the message is sent, so it holds the history the reply follows. Use a throwaway chat, because the files are committed. To record without paying for a model, start `rest_server` and `copilot_executor` with `CHAT_TEST_MODE=true`, which swaps the LLM for the backend's dummy service. `--session-out` and `--reply-out` write to other file names under `fixtures/`. A recorded fixture breaks the `fixtures.test.ts` check that the committed files match the generator, so update that test when you commit one.

## Adding a scenario

1. Add `scripts/perf/scenarios/<name>.ts` exporting a `Scenario` (`lib/scenario.ts`): a `name`, a `description`, a `readyWhen` sentence, a `windowMs`, and `setup(ctx)`. `setup` navigates and waits until the page is ready, then returns `{ run() }` with the measured interaction (`noInteraction` for an idle window).
2. Serve its data from a fixture: add the URL pattern to `ROUTE_TABLE` in `lib/routes.ts`, and the handler next to the other fixtures. Call `route.fallback()` for requests the fixture does not own.
3. Reuse the Playwright page objects in `src/playwright/pages/` for selectors.
4. Register it in `scenarios/registry.ts` and add a row to the table above.
5. Run it with `--runs 3 --screenshot`, check the screenshot, and check that renders per commit is stable across runs.
6. Add its baseline to [frontend-perf-baseline.md](frontend-perf-baseline.md).

---

### Engineering Graphiti Luna Prompt Evaluation (`docs/engineering/graphiti-luna-prompt-evaluation.md`)

# Graphiti Luna extraction prompt evaluation

Evaluated October 19, 2026, following #15656 and #15622. The scoped prompt change
improves durable-memory answer coverage over the Luna configuration on `dev`.
It does **not** establish that Luna never loses an answer compared with mini.

## Problem and change

Graphiti 0.30.2's entity system prompt forbids abstract concepts, while our custom
extraction instructions require specific Concept, Preference, and Rule entities.
Appending more instructions leaves the higher-priority prohibition in place.
The original Luna extraction also drops concrete details embedded in requests,
leaving recent episode text as their only retrievable copy.

The adapter replaces the system policy only for Luna's known entity and edge
extraction contracts. It permits subject-qualified anchors, retains concrete
values and qualifications, and distinguishes requested actions from completed
actions. Source messages, schemas, provenance, language instructions, retries,
model selection, and request parameters remain intact. Attribute extraction,
deduplication, other models, and the reranker retain their existing behavior.

This follows OpenAI's advice to [audit conflicting instructions and evaluate
prompts on the chosen model](https://developers.openai.com/api/docs/guides/latest-model#prompting-best-practices).
The existing non-reasoning request settings follow the [migration guidance](https://developers.openai.com/api/docs/guides/latest-model#migration-quickstart).
The application keeps [source data separated from instructions](https://developers.openai.com/api/docs/guides/prompt-engineering#message-formatting-with-markdown-and-xml),
without parsing or rewriting user-controlled delimiters.

## Method

- Base application: `dace3cbfba00fd247a030a03ce8596163d2f3311`.
- Dataset: [AgentProbe multi-session memory scenarios](https://github.com/Significant-Gravitas/agentprobe/blob/185e443aeecd0be3d5abdea201193513d8db62f8/data/multi-session-memory.yaml),
  31 scenarios, 60 ingested user messages, 35 answer probes, 141 atomic checks.
- Each configuration used fresh, isolated FalkorDB 4.18.9 graphs, actual
  production ingestion and warm-context retrieval, and live model calls.
- Mini used the pinned upstream OpenAI client with `gpt-4.1-mini` for both model
  sizes and the old reranker from `d772be3095b7888c65e12bcca6ed6c4b36acc4ee`.
  Other application code stayed on the same base; this is a model/prompt
  comparison, not a replay of the entire historical application.
- Original Luna used the current client and reranker with the new policy
  disabled. Migrated Luna used this policy. Both used `gpt-6-luna`, no reasoning,
  and unchanged extraction temperature/token settings. OpenRouter routed the
  model requests; embeddings used OpenAI `text-embedding-3-small`.
- A fixed Luna answerer used temperature zero and the same questions and answer
  instructions for all configurations. Expected answers were never sent to it.
  Objective per-field checks and manual inspection of misses supplied the scores.
- Normal memory included facts and up to five recent episodes. Fact-only memory
  suppressed recent episodes to expose missing durable facts. Memory-off supplied
  neither. These are three different tests, not interchangeable quality scores.
- Session gaps were replayed cumulatively from April 6, 2026. The reader was
  told the replay date. User turns were ingested directly; tool execution, the
  full chat loop, Dream, and production load were not part of this run.
- The final candidate was run in two disjoint concurrent scenario subsets.
  This is one sample per case, with correlated checks. It is not a statistical
  non-inferiority test, and prompt development used some of these cases.

## Results

The raw scorer counts all 141 checks, including known scorer/scope problems:

| Configuration | Normal memory | Facts only | Memory off |
| --- | ---: | ---: | ---: |
| Mini | 136/141 | 105/141 | 14/141 |
| Luna, original prompts | 133/141 | 81/141 | 14/141 |
| Luna, migrated prompts | 134/141 | 126/141 | 15/141 |

Adjudication excludes the two forgetting checks affected by a shared retrieval
defect reproduced in these runs, and two checks whose questions did not ask for those
fields (Notion subscription tier and the introspection company's name). The
company regex also accepted a substring of an email address. These exclusions
apply identically to every configuration; they leave 137 checks in 34 probes.

| Configuration | Normal memory | Facts only | Memory off | Entire normal probes | Entire fact-only probes |
| --- | ---: | ---: | ---: | ---: | ---: |
| Mini | 132/137 | 104/137 | 12/137 | 31/34 | 19/34 |
| Luna, original prompts | 129/137 | 79/137 | 12/137 | 30/34 | 16/34 |
| Luna, migrated prompts | 131/137 | 125/137 | 13/137 | 31/34 | 28/34 |

No normal or fact-only check that passed with original Luna failed with migrated
Luna in this comparison. Improvements include incidental details, taught
procedures, project totals, business identifiers, and multi-session recall.

Ten additional synthetic controls covered JSON and text input, subject
separation, units, one-off exceptions, unknown preferences, corrections,
conditional rules, and uncompleted requests. Mini scored 61/61 normal and 58/61
fact-only; migrated Luna scored 61/61 in both. Both scored 6/61 without memory.
Their raw scores were respectively 58/61 and 56/61 for mini, and 59/61 and 59/61
for Luna. Adjudication accepted equivalent CC/unknown-time/euro wording and
rejected "unknown whether placed" as evidence that an order was unplaced.
These controls include development cases and should not be treated as a fully
untouched holdout set.

[Per-probe scores and failed check names](graphiti-luna-prompt-evaluation.csv)
retain both raw and adjudicated results, including the excluded forgetting probe.

## Remaining differences and limitations

Two checks passed with mini but failed with the candidate:

1. **Pricing, normal memory:** mini returned the $24.99 wholesale rate but
   abstained on the discount and final price; Luna returned `UNKNOWN`. Both failed
   the complete probe. Luna's retrieved fact and recent episode contain $24.99,
   the 50-unit threshold, and the 10% first-order discount, but the fact bundles
   the general policy into an UrbanFit quote. Applying it to NorthStar remains
   unreliable.
2. **Shipping, facts only:** both retrieved the Reno warehouse and Tuesday/
   Thursday shipping rules. Mini correctly answered Friday–Monday for 3–4
   business-day transit after Tuesday dispatch; Luna answered Thursday–Friday.
   This is a reader calculation failure with the necessary memory present.

The fixed reader's instruction to use only supplied memory can cause excessive
abstention when an arithmetic input is in the question. A separate sensitivity
run allowing question-provided inputs improved some answers but remained
inconsistent. It does not replace the primary scores or prove the gaps fixed.
The interrupted-task continuation probe still fails in both configurations.
The shared fact-only forgetting failure remains in all three configurations.
It is distinct from the correction and briefing-recall examples tracked in #15655.

The opt-in supplier regression initially failed the message case on its minimum
order check. The old assertion did not retain the raw facts, so omission versus
equivalent number wording cannot be established. After accepting both `18` and
`eighteen` and printing synthetic facts on failure, a message repeat and the
complete three-source run passed. This remains a small stochastic sample.

A later wording experiment and two new scope controls hit the provider key's
daily limit (HTTP 402). That experiment was reverted; none of those incomplete
results are counted here. The shipped policy matches the fully evaluated
candidate hash below. The evidence supports improvement over current Luna,
but further representative repeats/full-chat evaluation are needed for a
blanket no-regression claim against mini.

## Cost, latency, and request validity

| Configuration | Ingestion median / max | Warm context median / max | Recorded model cost |
| --- | ---: | ---: | ---: |
| Mini | 7.00 / 13.73 s | 1.11 / 2.64 s | $0.26446 |
| Luna, original prompts | 8.63 / 15.50 s | 1.32 / 3.94 s | $0.10326 |
| Luna, migrated prompts | 10.89 / 17.94 s | 1.56 / 3.82 s | $0.11417 |

Costs include the common answerer and exclude direct embedding charges.
These concurrent local runs are not production percentiles. The prompt change
increases ingestion work; observed warm-context calls stayed inside the existing
eight-second budget. The largest natural ranking batch had 15 candidates.

A separate fixed 40-candidate ranking test used four relevant facts and 36
distractors. Luna's existing reranker put all four relevant facts first in all
three runs (3.53, 4.72, 3.10 seconds). Mini's old reranker retrieved two of four
in its top four (2.31, 2.04, 2.49 seconds). All runs returned 40 results within
eight seconds. This component result is not a load test or a reranker change.

The three primary runs completed with no HTTP or scenario errors. Their 347,
471, and 576 structured responses respectively validated against the requested
schemas. This validates output shape, not semantic completeness.

## Regression checks and reproduction

Initial evaluation checks:

- Graphiti non-integration suite: **330 passed, one pre-existing xfail**.
- Focused client/prompt suite: **65 passed**, 100% statement coverage of the
  prompt dispatcher and adapter; three paid tests skipped by default.
- Paid regression: **three passed**, covering message, JSON, and text ingestion
  into a fresh graph. It inspects stored relationships for six concrete details.
- Black, isort, Ruff, and scoped Pyright passed. Full application E2E was not run.

The first review follow-up passed **339 unit tests** with one pre-existing xfail.
New tests invoke graphiti-core's real node/edge extraction entry points and reject
an inactive dispatcher. Four assertion regressions were reproduced before fixing
row-order dependence and cross-fact matches. Detail checks now match individual
facts and require each dispatch day in a dispatch-related fact. CI's complete
Python 3.11/3.12/3.13 reports validate after registering the three intentional
paid-test skips. The paid run above predates the stricter assertions; it was not
repeated after the provider's daily quota was exhausted.

The assertion follow-ups passed **398 unit tests** with one pre-existing xfail.
Thirty false passes were reproduced before adding exact payment-value checks
and scenario-specific checks for invented signing claims and explicit company
misattribution. Payment values must directly follow a payment term, and explicit
counterparty detail claims include the "has" form. Signing instructions cannot
hide a later affirmative claim across "but" or "and"; punctuation-boundary
controls also pass. Two false rejections of negated instructions with coordinated
subjects were also reproduced and fixed. Positive controls allow negated signing
statements, source instructions, and supplier briefs naming the counterparty.
The phrase "for Elm Studio" alone can describe the requested brief's context and is
not treated as an explicit claim about that company's own terms. These pattern
checks cover the demonstrated wording; they are not a general semantic judge. Offline
replay of four saved migrated-Luna runs of the incidental-detail scenario passed
the tightened checks. This replay made no new model calls and does not replace
a fresh three-source paid integration run.

The full pre-commit invocation encountered local Windows/WSL tooling failures:
Bash could not resolve the Windows worktree metadata or find Poetry/Node, and the
isort hook's Poetry option was unsupported locally. Native scoped equivalents
passed. The hook's unscoped typecheck also scanned Classic and reported 2,980
errors, including unresolved Classic imports; it is not a passing backend-wide
typecheck. CI must validate the repository-wide hooks.

From `autogpt_platform/backend`, with an isolated FalkorDB and configured test
model/embedding credentials, set `RUN_GRAPHITI_LLM_EVAL=1`, select
`GRAPHITI_LLM_MODEL=gpt-6-luna`, and run:

```sh
poetry run pytest backend/copilot/graphiti/extraction_quality_integration_test.py -q
```

`GRAPHITI_FALKORDB_HOST`/`PORT`/`PASSWORD` select the test database service. The
fixture creates a unique graph, cleans its data, and closes clients. This command
makes paid calls. It reproduces the durable-detail regression, not the complete
adapted benchmark. The full request captures and local harness are not committed;
the CSV is a score export, not an executable benchmark. Temporary test services
were removed after validation.

## Evidence fingerprints

SHA-256 identifies the evaluated artifacts; source hashes use LF bytes. The
review follow-up changes only comments in the prompt module: its syntax tree and
prompt strings match the evaluated revision.

| Artifact | SHA-256 |
| --- | --- |
| Evaluated `extraction_prompts.py` at `51cdfdd` | `34b04d8c8d12475d77d9c1b90414a78ddf5ac86434c0f2c6d853f9f63475c5cd` |
| AgentProbe source YAML | `faf01e2ec24135acc3fb558bee02ad1a31faf999919b7a854fb77245e563412b` |
| Adapted probe/check definitions | `413edbfbd714941a9e578fb52c31d1593c2886de5b245c27d054cb0ff25f503f` |
| Mini primary capture | `91415816d889790d21d525d5bb6e70f168ad375701c63040293db5fc3f16722a` |
| Original Luna primary capture | `e41139bfef13d900fb6bbd927b4eea395097d962cc5d4c7005c2e19163adb1dc` |
| Migrated Luna primary capture | `fafe5822dba724ad0eb6872d022caa8ecad0fb61c6e3381be1559fba02e2ca02` |

---

### Engineering Hosted Mcp Catalog (`docs/engineering/hosted-mcp-catalog.md`)

# Maintaining MCP Integrations

[`mcp_catalog.json`](../../autogpt_platform/backend/backend/integrations/mcp_catalog.json) defines the official services shown in Integrations and the AutoPilot MCP guide. [`mcp_catalog.py`](../../autogpt_platform/backend/backend/integrations/mcp_catalog.py) validates the records and exposes them through `get_mcp_catalog()`.

## Add or update a service

- Keep a stable `mcp_...` ID and link directly to the vendor's setup documentation.
- Describe the server's purpose and required plan, admin approval, region, endpoint, and credentials in `description` and `setup_instructions`.
- Choose the connection setup that matches the vendor's server; see [Choose how users connect](#choose-how-users-connect).
- Order the supported `auth_methods` by preference; the first method is selected initially. Choices are `oauth`, `bearer`, `basic`, and `none`.
- Set `oauth_server_url` when the vendor supplies a separate signed-in endpoint.
- Set documented `oauth_scopes` for the initial grant and separate `oauth_write_scopes` for the optional **Allow changes** choice. An empty default list omits the scope parameter; omission/null uses discovery defaults.

## Choose how users connect

Match the entry to where the vendor runs its MCP server. Every URL must be a public HTTPS URL.

- **One public URL for every customer:** use `connection_mode: hosted` with `server_url`. The connect dialog shows the URL locked, and AutoPilot uses it. Examples: PostHog, Notion.
- **One public URL, and the service can also be self-hosted:** use `hosted` with `server_url` and `allow_custom_url: true`. The dialog opens on `server_url`, and users can replace it with their own instance's URL. AutoPilot still uses `server_url`. Say in `setup_instructions` how to connect a self-hosted instance, including any sign-in setup it needs. Example: OpenSEO.
- **A separate URL per region:** use `connection_mode: custom` with one `server_url_options` entry per region and no `server_url`. Users pick their region, and the picker's **Custom URL** option covers self-hosted instances. Prefer this to one entry per region. Don't make it a hosted entry that defaults to one region, because users in the other regions would connect to the wrong one. Examples: Amplitude, Langfuse.
- **A URL unique to each customer account:** use `connection_mode: custom` with no `server_url` and no options. Say in `setup_instructions` where users find their URL. The dialog opens with an empty URL field. Examples: Chargebee, Sourcegraph.

Both `custom` setups show **Setup required** in the integrations list. AutoPilot tells users to add their URL under Settings → Integrations before it can use the server.

## Validate the change

Check the vendor's current endpoint and authentication contract. Verify initialization and tool listing with the MCP client, then a benign read using the intended permissions. For OAuth, check registration, consent, and refresh with the deployment's callback and an authorized test account.

Update affected catalog, provider, guide, and page integration tests. Cover changes to region selection, custom URLs, token handling, or optional grants. Regenerate API types when the metadata schema changes, and run the relevant backend and frontend checks.

---

### Engineering Oauth Integration Flow (`docs/engineering/oauth-integration-flow.md`)

# OAuth Integration Flow Documentation

## Overview

The AutoGPT platform implements OAuth 2.0 in two distinct contexts:

1. **User Authentication (SSO)**: Handled by Better Auth, embedded in the Next.js frontend at `/api/auth/*`
2. **API Integration Credentials**: Custom OAuth implementation for third-party service access

This document focuses on the **API Integration OAuth flow** used for connecting to external services. For the list of supported providers, see `autogpt_platform/backend/backend/integrations/providers.py`. For user authentication documentation, see `autogpt_platform/frontend/src/lib/auth/SESSION_VALIDATION.md`.

## Trust Boundaries

### 1. Frontend Trust Boundary
- **Location**: Browser/Client-side application
- **Components**: 
  - `CredentialsInput` component (`autogpt_platform/frontend/src/components/contextual/CredentialsInput/CredentialsInput.tsx`)
  - OAuth callback route (`autogpt_platform/frontend/src/app/(platform)/auth/integrations/oauth_callback/route.ts`)
- **Trust Level**: Untrusted - user-controlled environment
- **Security Measures**:
  - CSRF protection via state tokens
  - Popup-based flow to prevent URL exposure
  - Message validation for cross-window communication

### 2. Backend API Trust Boundary
- **Location**: Server-side FastAPI application
- **Components**:
  - Integration router (`autogpt_platform/backend/backend/api/features/integrations/router.py`)
  - OAuth handlers (`autogpt_platform/backend/backend/integrations/oauth/`)
  - Credentials store (`autogpt_platform/backend/backend/integrations/credentials_store.py`)
- **Trust Level**: Trusted - server-controlled environment
- **Security Measures**:
  - JWT-based authentication
  - Encrypted credential storage
  - Token refresh handling
  - Scope validation

### 3. External Provider Trust Boundary
- **Location**: Third-party OAuth providers
- **Components**: Provider authorization endpoints
- **Trust Level**: Semi-trusted - external services
- **Security Measures**:
  - HTTPS-only communication
  - Provider-specific security features
  - Token revocation support

## Component Architecture

### Frontend Components

#### 1. CredentialsInput Component
- **Purpose**: UI component for credential selection and OAuth initiation
- **Key Functions**:
  - Displays available credentials
  - Initiates OAuth flow via popup window
  - Handles OAuth callback messages
  - Manages credential selection state

#### 2. OAuth Callback Route
- **Path**: `/auth/integrations/oauth_callback`
- **Purpose**: Receives OAuth authorization codes from providers
- **Flow**:
  1. Receives `code` and `state` parameters from provider
  2. Posts message to parent window with results
  3. Auto-closes popup window

### Backend Components

#### 1. Integration Router
- **Base Path**: `/api/integrations`
- **Key Endpoints**:
  - `GET /{provider}/login` - Initiates OAuth flow
  - `POST /{provider}/callback` - Exchanges auth code for tokens
  - `GET /credentials` - Lists user credentials
  - `DELETE /{provider}/credentials/{id}` - Revokes credentials

#### 2. OAuth Base Handler
- **Purpose**: Abstract base class for provider-specific OAuth implementations
- **Key Methods**:
  - `get_login_url()` - Constructs provider authorization URL
  - `exchange_code_for_tokens()` - Exchanges auth code for access tokens
  - `refresh_tokens()` - Refreshes expired access tokens
  - `revoke_tokens()` - Revokes tokens at provider

#### 3. Credentials Store
- **Purpose**: Manages credential persistence and state
- **Key Features**:
  - Redis-backed mutex for concurrent access control
  - OAuth state token generation and validation
  - PKCE support with code challenge generation
  - Default system credentials injection

## OAuth Flow Sequence

### 1. Flow Initiation
```mermaid
sequenceDiagram
    participant User
    participant Frontend
    participant Backend
    participant Redis
    participant Provider

    User->>Frontend: Click "Sign in with Provider"
    Frontend->>Backend: GET /api/integrations/{provider}/login
    Backend->>Redis: Store state token + code verifier
    Backend->>Frontend: Return login URL + state token
    Frontend->>Frontend: Open popup window
    Frontend->>Provider: Redirect to authorization URL
```

### 2. Authorization
```mermaid
sequenceDiagram
    participant User
    participant Provider
    participant Callback
    participant Frontend
    participant Backend

    User->>Provider: Authorize application
    Provider->>Callback: Redirect with code + state
    Callback->>Frontend: PostMessage with code + state
    Frontend->>Backend: POST /api/integrations/{provider}/callback
    Backend->>Provider: Exchange code for tokens
    Provider->>Backend: Return access + refresh tokens
    Backend->>Backend: Store credentials
    Backend->>Frontend: Return credential metadata
```

### 3. Token Refresh
```mermaid
sequenceDiagram
    participant Application
    participant Backend
    participant Provider

    Application->>Backend: Request with credential ID
    Backend->>Backend: Check token expiry
    Backend->>Provider: POST refresh token
    Provider->>Backend: Return new tokens
    Backend->>Backend: Update stored credentials
    Backend->>Application: Return valid access token
```

## System Architecture Diagram

```
/* Detailed source-code truncated for AI context efficiency. */
```

## Data Flow Diagram

```mermaid
graph LR
    subgraph "Data Types"
        ST[State Token]
        CV[Code Verifier]
        CC[Code Challenge]
        AC[Auth Code]
        AT[Access Token]
        RT[Refresh Token]
    end
    
    subgraph "Frontend Flow"
        U1[User Initiates]
        U2[Receives State]
        U3[Opens Popup]
        U4[Receives Code]
        U5[Sends to Backend]
    end
    
    subgraph "Backend Flow"
        B1[Generate State]
        B2[Store in Redis]
        B3[Validate State]
        B4[Exchange Code]
        B5[Store Credentials]
    end
    
    U1 --> B1
    B1 --> ST
    B1 --> CV
    CV --> CC
    B2 --> U2
    U3 --> AC
    AC --> U4
    U5 --> B3
    B3 --> B4
    B4 --> AT
    B4 --> RT
    AT --> B5
    RT --> B5
```

## Security Architecture

```mermaid
graph TB
    subgraph "Security Layers"
        subgraph "Transport Security"
            HTTPS[HTTPS Only]
            CSP[Content Security Policy]
        end
        
        subgraph "Authentication"
            JWT[JWT Tokens]
            STATE[CSRF State Tokens]
            PKCE[PKCE Challenge]
        end
        
        subgraph "Storage Security"
            ENC[Encrypted Credentials]
            SEC[SecretStr Type]
            MUTEX[Redis Mutex Locks]
        end
        
        subgraph "Access Control"
            USER[User Scoped]
            SCOPE[OAuth Scopes]
            EXPIRE[Token Expiration]
        end
    end
    
    HTTPS --> JWT
    JWT --> USER
    STATE --> PKCE
    PKCE --> ENC
    ENC --> SEC
    SEC --> MUTEX
    USER --> SCOPE
    SCOPE --> EXPIRE
```

## Credential Lifecycle

```mermaid
stateDiagram-v2
    [*] --> Initiated: User clicks sign-in
    Initiated --> Authorizing: Popup opened
    Authorizing --> Authorized: User approves
    Authorizing --> Failed: User denies
    Authorized --> Active: Tokens stored
    Active --> Refreshing: Token expires
    Refreshing --> Active: Token refreshed
    Refreshing --> Expired: Refresh fails
    Active --> Revoked: User deletes
    Failed --> [*]
    Expired --> [*]
    Revoked --> [*]
    
    note right of Active: Credentials can be used
    note right of Refreshing: Automatic process
    note right of Revoked: Tokens revoked at provider
```

## OAuth Types Comparison

### User Authentication (SSO) via Better Auth

- **Purpose**: Authenticate users to access the AutoGPT platform
- **Provider**: Better Auth, embedded in the Next.js frontend (supports Google, GitHub, and Discord SSO)
- **Flow Path**: `/login` → `/api/auth/*` (Better Auth) → provider OAuth → back to the app
- **Session Storage**: Better Auth-managed cookies
- **Token Management**: Automatic by Better Auth; the Python backend validates JWTs via the JWKS endpoint (`/api/auth/jwks`)
- **User Experience**: Single sign-on to the platform

### API Integration Credentials

- **Purpose**: Grant AutoGPT access to user's third-party services
- **Providers**: Examples include GitHub, Google APIs, Notion, and others
  - Full list in `autogpt_platform/backend/backend/integrations/providers.py`
  - OAuth handlers in `autogpt_platform/backend/backend/integrations/oauth/`
- **Flow Path**: Integration settings → `/api/integrations/{provider}/login` → `/auth/integrations/oauth_callback`
- **Credential Storage**: Encrypted in PostgreSQL
- **Token Management**: Custom refresh logic with mutex locking
- **User Experience**: Connect external services to use in workflows

## Data Flow and Security

### 1. State Token Flow

- **Generation**: Random 32-byte token using `secrets.token_urlsafe()`
- **Storage**: Redis with 10-minute expiration
- **Validation**: Constant-time comparison using `secrets.compare_digest()`
- **Purpose**: CSRF protection and request correlation

### 2. PKCE Implementation

- **Code Verifier**: Random string generated using `secrets.token_urlsafe(128)` (approximately 171 characters when base64url encoded, though RFC 7636 recommends 43-128 characters)
- **Code Challenge**: SHA256 hash of verifier, base64url encoded
- **Storage**: Stored with state token in database (encrypted) with 10-minute expiration
- **Usage**: Enhanced security for public clients (currently used by Twitter provider)

### 3. Credential Storage

- **Structure**:

  ```python
  OAuth2Credentials:
    - id: UUID
    - provider: ProviderName
    - access_token: SecretStr (encrypted)
    - refresh_token: Optional[SecretStr]
    - scopes: List[str]
    - expires_at: Optional[int]
    - username: Optional[str]
  ```

- **Persistence**: PostgreSQL via Prisma ORM
- **Access Control**: User-scoped with mutex locking

### 4. Token Security

- **Storage**: Tokens stored as `SecretStr` type
- **Transport**: HTTPS-only, never logged
- **Refresh**: Automatic refresh 5 minutes before expiry
- **Revocation**: Supported for providers that implement it

## Provider Implementations

### Supported Providers

The platform supports various OAuth providers including GitHub, Google, Notion, Twitter, and others. For the complete list, see:
- `autogpt_platform/backend/backend/integrations/providers.py` - All supported providers
- `autogpt_platform/backend/backend/integrations/oauth/` - OAuth implementations

### Provider-Specific Security Considerations

- **GitHub**: Supports optional token expiration - tokens may be non-expiring by default
- **Linear**: Returns scopes as space-separated string, requiring special parsing
- **Google**: Requires explicit offline access scope for refresh tokens
- **Twitter**: Uses PKCE for enhanced security on public clients

Each provider handler implements the security measures defined in `BaseOAuthHandler`, ensuring consistent token management and refresh logic across all integrations.

## Security Best Practices

### 1. Frontend Security

- Use popup windows to prevent URL tampering
- Validate state tokens before processing callbacks
- Clear sensitive data from window messages
- Implement timeout for OAuth flows (5 minutes)

### 2. Backend Security

- Store client secrets in environment variables
- Use HTTPS for all OAuth endpoints
- Implement proper scope validation
- Log security events without exposing tokens
- Use database transactions for credential updates

### 3. Token Management

- Refresh tokens proactively (5 minutes before expiry)
- Revoke tokens when credentials are deleted
- Never expose tokens in logs or error messages
- Use constant-time comparison for token validation

## Error Handling

### Common Error Scenarios

1. **Invalid State Token**: 400 Bad Request
2. **Provider Configuration Missing**: 501 Not Implemented
3. **Token Exchange Failure**: 400 Bad Request with hint
4. **Webhook Conflicts**: 409 Conflict, requires confirmation
5. **Credential Not Found**: 404 Not Found

### Error Response Format

```json
{
  "detail": {
    "message": "Human-readable error description",
    "hint": "Actionable suggestion for resolution"
  }
}
```

## Testing Considerations

### Unit Testing

- Mock OAuth providers for flow testing
- Test state token generation and validation
- Verify PKCE implementation
- Test concurrent access scenarios

### Integration Testing

- Use provider sandboxes when available
- Test full OAuth flow with real providers
- Verify token refresh mechanisms
- Test error scenarios and recovery

### Logging Guidelines

- Log flow initiation and completion
- Log errors with context (no tokens)
- Track provider-specific issues
- Monitor for suspicious patterns

---

### Engineering Org Feature Map (`docs/engineering/org-feature-map.md`)

# Organization Feature Map

A capability map for org/team tenancy on the AutoGPT Platform: what the
foundation shipped in the org-workspace PR series provides, and the feature
surface we expect to build on top of it. Each section lists the existing
hooks in the codebase that a feature would attach to, so scoping starts from
real anchor points instead of a blank page.

## Foundation (shipped)

- **Schema**: `Organization`, `Team`, `OrgMember`, `TeamMember`,
  `OrgInvitation`, `TeamInvite`, `OrganizationProfile`, `OrganizationAlias`,
  `OrgBalance`, `OrgCreditTransaction`, `OrganizationSubscription`,
  `OrganizationSeatAssignment`, `TransferRequest`, `AuditLog`
- **Request context**: `RequestContext` (`org_id`, `team_id`, role flags,
  `seat_status`) resolved per-request via `get_request_context`, with
  `X-Org-Id` / `X-Team-Id` header overrides and personal-org fallback
- **Execution context**: `ExecutionContext.organization_id` / `team_id`
  threaded through graph executions, webhooks, schedules, copilot runs
- **Billing**: `get_credit_model(user_id, org_id)` routes to
  `OrgCreditModel` when an org is active; executor charging follows
- **Resources**: graphs, executions, API keys, chat sessions, store
  listings carry `organizationId` / `teamId`; marketplace listings support
  org ownership and cross-org transfers
- **Frontend**: org/team switcher in the navbar, `OrgTeamProvider`
  bootstraps context on login, zustand store persists the active org/team
- **Migration**: idempotent personal-org bootstrap
  (`create_orgs_for_existing_users`) behind a Redis lock at startup

Deferred from the foundation series: seat enforcement (paywall/subscription
gating work owns this) and the NOT-NULL cutover migration for
`organizationId` columns.

## 1. Governance & policy controls

The headline ask: org/team admins control what members can do and which
capabilities agents may use.

| Feature | Sketch |
| --- | --- |
| **Block enable/disable per org/team** | New `BlockPolicy` table (`orgId`, `teamId?`, `blockId`, `allowed`, optional reason). Enforcement at three choke points: `get_graph_blocks` (filter the palette), `validate_graph` (reject save/activation of graphs using denied blocks), and executor node resolution (defense-in-depth refusal at run time, since policies can change after a graph is saved). Admin UI under org settings; team policy overrides org policy only toward more-restrictive. |
| **Provider/credential policy** | Same pattern keyed on provider name instead of block ID: restrict which integrations (HTTP request, code execution, specific SaaS providers) teams may attach credentials for. `IntegrationCredential` already carries an org relation to hang this on. |
| **Model allowlist + per-team spending caps** | Policy keyed on LLM model family; enforced where block costs resolve (`block_usage_cost`) and in copilot model selection. Caps check `OrgCreditTransaction` aggregates per team before dispatch. |
| **Sensitive-action approval routing** | `ExecutionContext.sensitive_action_safe_mode` and human-in-the-loop review already exist per-user; org version routes pending reviews to org/team admins instead of only the owner, using the existing review queue. |
| **Safe-mode defaults per org** | Org `settings` JSON already exists on `Organization`; add admin-managed defaults that seed `human_in_the_loop_safe_mode` / `sensitive_action_safe_mode` for all member executions, member-overridable only if policy allows. |

## 2. Identity & access

| Feature | Sketch |
| --- | --- |
| **SSO (SAML/OIDC)** | Supabase auth supports external IdPs; org-level config maps IdP groups → teams. `OrganizationAlias` (domain aliases) is the natural anchor for domain-capture ("anyone @acme.com joins Acme org"). |
| **SCIM provisioning** | Sync engine writing to `OrgMember`/`TeamMember`; deprovisioning must also revoke API keys and seats (`OrganizationSeatAssignment`). |
| **Custom roles / RBAC** | Today roles are three booleans (`isOwner`, `isAdmin`, `isBillingManager`) on both org and team membership. A `Role` table with permission grants replaces the booleans when customers need finer slicing (e.g. "can publish to marketplace but not manage billing"). `RequestContext` already centralizes the flag derivation, so the cutover is contained. |
| **Audit log surfacing** | `AuditLog` model exists with org relation; needs consistent write coverage across mutating routes (member changes, policy changes, transfers, key creation/revocation) and an admin-facing viewer with filtering/export. |
| **Session & network policy** | Org-enforced re-auth interval, IP allowlists for API-key traffic (check at `validate_api_key` where org context is already resolved). |

## 3. Resource management & collaboration

| Feature | Sketch |
| --- | --- |
| **Team-shared agent library** | `AgentGraph.teamId` exists; the library views currently scope by user. Team-visible library = library queries keyed by team membership plus `visibility` (enum already on graphs/library agents/chat sessions). |
| **Org-shared credentials** | `IntegrationCredential` already relates to `Organization`; needs scoping rules (org-owned credentials usable by member teams), admin management UI, and executor credential resolution preferring team → org → user. |
| **Environment promotion (dev → prod teams)** | `TransferRequest` covers cross-org moves; intra-org promotion is a lighter copy with provenance (`forkedFromId` already tracked on graphs). |
| **Quotas per team** | Run-concurrency and storage ceilings checked in `add_graph_execution` (org/team already threaded there) and workspace upload paths. |
| **Shared chat / handoff** | Chat sessions carry `organizationId`/`teamId` and a sharing layer (share tokens, linked execution shares) exists; team-visible sessions are a `visibility` policy on top. |

## 4. Billing & finance

| Feature | Sketch |
| --- | --- |
| **Seat management & enforcement** | Schema shipped (`OrganizationSubscription`, `OrganizationSeatAssignment`, `seat_status` on `RequestContext`); enforcement intentionally deferred to the paywall/subscription workstream. |
| **Budgets & alerts per team** | Aggregate `OrgCreditTransaction` by team; thresholds trigger the existing notification pipeline (low-balance handling already has a user-level analog in the executor). |
| **Cost attribution dashboards** | Transactions already record user, team, graph, and execution metadata — the work is aggregation endpoints + admin UI, not new write paths. |
| **Org auto-top-up** | `Organization.topUpConfig` field exists; needs the org analog of `set_auto_top_up` plus Stripe customer on org (`stripeCustomerId` present). |
| **Invoicing (PO / net terms / tax IDs)** | Stripe invoice plumbing per org customer; `OrgCreditModel.list_invoices` is stubbed for this. |

## 5. Marketplace

| Feature | Sketch |
| --- | --- |
| **Org-private marketplace** | Store listings already support `owningOrgId`; an org-internal visibility tier reuses the listing/version review flow with org-scoped queries instead of the public `StoreAgent` view. |
| **Publishing approval chain** | Submission review exists for platform admins; org version inserts an org-admin approval step before a listing leaves the org boundary. |
| **Listing transfers** | Shipped (`TransferRequest` source/target org with admin approval on both ends). |

## 6. Observability & compliance

| Feature | Sketch |
| --- | --- |
| **Org-wide run history & analytics** | Executions carry org/team; admin endpoints mirror the existing per-user execution listings with org-role authorization. |
| **Data retention policy** | Org `settings`-driven TTL sweeping executions/chat sessions; deletion paths (cascade rules on shares, workspaces) already exist to build on. |
| **Export / portability** | Org-scoped bulk export of graphs (graph export logic exists per-graph), execution history, and transactions. |
| **Compliance evidence** | Once `AuditLog` coverage is complete, scheduled export bundles cover the SOC2-style asks. |

## Suggested build order

1. **Block/provider policy** (per-team enable/disable) — highest admin demand,
   clear enforcement choke points, no schema risk beyond one table
2. **Audit log write coverage + viewer** — prerequisite trust layer for
   everything else admins touch
3. **Team-shared library + org credentials** — turns tenancy from isolation
   into collaboration; biggest day-to-day user value
4. **Budgets/caps + cost attribution** — natural follow-on to org billing
   already in place
5. **RBAC generalization** — only when boolean roles demonstrably block a
   customer; cutover is contained in `get_request_context`
6. **SSO/SCIM** — enterprise gate; heavy lift, schedule against real demand

---

### Engineering Pro Activation (`docs/engineering/pro-activation.md`)

# Trial conversion to Pro or Max and usage

This backend change is stacked on allowance PR #15184, tested parent
`7a1b770438bd9315f685850ebadacedd09a27800`, rebased on dev
`f62abe332b4e065815d55e56a2faa5d1915fb5bd`. It gives an eligible trial's first settled
conversion to Pro or Max fresh daily and weekly consumption. Trial exhaustion
never initiates a purchase. The trial's original accepted schedule can convert automatically;
ending it early requires the separate explicit confirmation below.

## Durable activation and accounting

An owned, live Stripe subscription must be active, with an authoritative settled
recurring invoice for that subscription. A checkout redirect, completed Checkout
Session, trial setup invoice, subscription tier, or pending PaymentIntent does
not establish readiness. Settlement can include a valid discount or customer
credit; cash `amount_paid > 0` is not required.

`PaidUsageActivation` records one initial trial-conversion subscription/invoice identity per user.
The user row is locked before the trial row. Entitlement, conversion identity,
and the completed generation are committed together after Redis can read the
new generation's calendar counters. No daily or weekly key is deleted or zeroed.
A repeated reconciliation probes the same generation, preserving any paid usage
already written to it. Failed transactions can retry without destructive cleanup.

The generation changes the namespace of the existing UTC daily and ISO-week
counters. Configured Pro and Max limits and reset times stay the same. Work captures its
billing origin before execution and propagates it into background and child work.
Database FIFO turns persist that snapshot when queued and restore it at promotion,
even when activation happens while they wait for a running slot.
Held approvals retain their original snapshot for both the resumed turn and each
approved action, including actions held before conversion and answered afterward.
Delayed trial costs update the original lifetime trial ledger and retain their
original usage attribution. Neither conversion nor recovery clears trial history,
credits, runs, conversations, or historical costs.

Only a consumed, owned trial that has not already converted can qualify for a new
generation. A tier label alone cannot establish that history. These transitions
have explicit behavior:

| Transition | Fresh daily and weekly usage |
| --- | --- |
| Unconverted trial → first paid Pro | Yes, after settled payment and completed reconciliation. |
| Unconverted trial → first paid Max | Yes, after settled payment and completed reconciliation. |
| Paid Pro → Max | No; preserve consumption. |
| Paid Max → Pro | No; preserve consumption. |
| Unpaid account without a consumed trial → paid signup | No; preserve consumption. |
| Previously converted trial, renewal, returning paid subscriber, or admin tier edit | No new activation. |

Retries of a qualifying conversion finish the same activation and retain all paid
usage already written. The migration records the policy start in
`PaidUsageActivationPolicy` at Stripe's whole-second precision; pre-policy
payments are not retroactively reset.
An established paid-plan change can reconcile from a settled proration invoice
with owned recurring service at the current price and qualifying prior paid
history. Prorations never establish the initial-payment evidence for a reset.
Authoritative database reads bypass process-local tier caches for enforcement.
If entitlement or accounting state cannot be established, execution fails closed.

## Authenticated API

All routes below require the existing user bearer authentication and subscription
status rate limiter. Attempt IDs are always scoped to the authenticated user.
Foreign IDs return 404. The exported OpenAPI schema and generated client include
these endpoints. The historical `pro-activation` route and storage identifiers
remain compatible while supporting both Pro and Max. This PR contains no visual redesign.

| Method and exact endpoint | Request | Effect |
| --- | --- | --- |
| `POST /api/credits/pro-activation/preview` | `{"plan":"MAX","return_to":"/chat/THREAD?resume=1"}` | Preview conversion of the user's existing unconverted trial to Pro or Max and persist quoted terms. Never starts payment. |
| `POST /api/credits/pro-activation/{id}/confirm` | `{"confirmed":true,"terms_token":"HEX_SHA256"}` | Record explicit consent, then end the same trial subscription early with a stable idempotency key. |
| `GET /api/credits/pro-activation/{id}` | None | Retrieve live payment state and retry entitlement/usage reconciliation. Never creates or retries a charge. |
| `GET /api/credits/pro-activation/current` | None | Recover the user's persisted attempt after navigation, or reconcile their existing paid Pro/Max subscription. Never creates or retries a charge. |

Preview accepts an application-relative `return_to` path, including its query and
fragment; external destinations are rejected. The default is `/settings/billing`.
The optional `plan` is `PRO` or `MAX`; omitting it retains the trial's accepted
plan when that offer is Pro or Max. Other accepted trial offers require an
explicit Pro/Max target for this flow. Keeping the original plan preserves its
originally accepted price even if the current offer has changed. Selecting another
plan previews its configured price in the trial's accepted currency and billing
interval and requires fresh explicit confirmation of those terms. A saved preview
retains its selected price through confirmation and recovery even if the catalog
changes; expiry and charge, discount, and tax revalidation still apply. A fresh
preview uses the current catalog. The trial's original offer remains in its
lifetime history. A confirmed request changes the existing subscription item,
never creates a second subscription, and uses exactly the item/price previewed.
Once confirmed, the persisted destination and accepted terms are immutable.
Initial paid signup continues through the existing authenticated
`POST /api/credits/subscription` and Stripe Checkout, which presents the charge and
requires payment confirmation. Signup without a consumed trial does not reset
consumption; successful recovery reports `usage_reset=false`. Its validated success
destination is also saved in subscription metadata for `/current` recovery. Destinations exceeding Stripe's
metadata length limit are recovered from the owned Checkout's success URL after
checking the application origin.

Every successful activation response has these fields. Optional values are JSON
`null`; `return_to` always has a value and `usage_reset` defaults to false:

| Field | Type / meaning |
| --- | --- |
| `id` | Attempt ID, or null for initial paid signup without an early-conversion attempt. |
| `status` | One of the states in the next table. |
| `terms` | The persisted commercial terms below, or null for the existing Checkout flow. |
| `terms_token` | SHA-256 of the exact persisted terms, required by confirm; otherwise null. |
| `return_to` | Validated application-relative return destination. |
| `invoice_id` | Authoritative current invoice ID, when available. A `ready` response uses the invoice verified during reconciliation, even if it changed after the initial status read. |
| `hosted_invoice_url` | Stripe's hosted URL for the existing invoice when payment/action is required. |
| `retry_after_seconds` | Suggested status polling delay, normally 3 while processing. |
| `error_code` | Machine-readable diagnostic, otherwise null. |
| `usage_reset` | True only for `ready` when this subscription's current invoice and price match its completed initial trial conversion. False for nontrial signup, renewals, paid-plan changes, and returning/pre-policy/legacy conversions without an activation. It describes the original completed conversion; this poll never resets consumption. |
| `activation_id` | Stable completed initial activation ID when `usage_reset` is true; otherwise null. Repeated polling returns the same ID and never resets counters again. |

Before showing confirmation, display the commercial `terms`: `plan` (`PRO` or `MAX`),
`amount_due` formatted in `currency`, `billing_interval`
(`month` or `year`), `billing_interval_count`, `charge_timing`
(`on_confirmation`), `renewal_unit_amount`, and `renewal_terms`.
`price_id` identifies the selected paid price. `accepted_offer_token` binds the
conversion to the originally accepted trial offer without rewriting that offer;
`expires_at` is the UTC quote expiry, 15 minutes after preview.

Also display `renewal_discounts`: each contains `amount_off`, `percent_off`,
`currency`, `duration`, `duration_in_months`, and `ends_at` (Unix seconds when
available). A `once` discount applies to the initial invoice, not every renewal.
Display `renewal_tax.automatic`, `renewal_tax.price_tax_behavior`, and any explicit
`renewal_tax.rates` (`display_name`, `percentage`, `inclusive`, `country`, `state`).
These modifiers are included in the token and revalidated even when today's
amount happens to be unchanged. `amount_due` comes from a Stripe invoice preview
with the existing subscription's discounts, taxes, and customer credit. The base
renewal amount and modifiers explain subsequent charges without promising a
fixed future tax amount or an indefinitely recurring introductory discount.

| State | Client behavior |
| --- | --- |
| `confirmation_required` | Show the complete terms. Send confirm only after an explicit user action. Missing, false, string, or numeric confirmation is rejected. |
| `payment_required` | Open `hosted_invoice_url` to pay or update the payment method for this invoice. Retain the attempt and destination; poll status after return. Do not open another Checkout. |
| `action_required` | Complete authentication using the same hosted invoice. Then poll status; authentication itself is not readiness. |
| `processing` | Keep the activation recoverable and poll GET using the delay. Do not announce failure or fresh allowance. A settled payment can remain here while database/Redis reconciliation recovers. |
| `ready` | Paid Pro/Max entitlement and the applicable reset decision are authoritative. Refetch the three data sets below, then continue to `return_to`. Announce an initial fresh allowance only when `usage_reset=true`, once per `activation_id`. |
| `not_applicable` | The owned live subscription is outside Pro/Max (`not_paid_activation_subscription`), or its paid price has changed away from this attempt's accepted target (`plan_changed`). Stop activation polling, refetch subscription/usage, and continue to the preserved `return_to`. This is not a failed payment and grants no new reset. |
| `failed` | An authoritative canceled/expired subscription or void/uncollectible invoice has ended this attempt (`payment_canceled`). No fresh usage is granted. |

401/403 require authentication; 404 means no owned attempt/subscription was found.
409 means the unconfirmed terms expired/changed, ownership cannot be established,
or another confirmation currently owns the account checkout lock. Fetch current
state before deciding whether a fresh preview is appropriate. 422 means invalid
input. A preview/terms-validation Stripe failure can return 502 before payment is
submitted. After durable consent, an uncertain mutation or reconciliation result
returns `processing`, not a payment-failure claim. The current-status route also
returns `processing` during database/Stripe outages.
An unknown price or ambiguous subscription items remain `processing` with
`error_code=plan_unavailable`; a stale local tier alone never makes paid Pro/Max
recovery fail. Accepted trial prices remain recognized after offer changes.
If the plan changes during reconciliation, readiness waits for the locked tier,
current subscription price, and attempt's accepted target to agree. A subsequent
poll observes any changed attempt target as `not_applicable` (`plan_changed`).
`/current` skips subscriptions outside Pro/Max while looking for an applicable paid
subscription, returning `not_applicable` if no applicable subscription exists.

On refresh/navigation, call `/current`; do not derive success from URL parameters,
an old tier response, or a cached completed Checkout. If the original confirmation
request had no definitive response, it can be retried with the **same** persisted
attempt ID and terms token. A live subscription that already left `trialing` is
only reconciled. If still trialing, the exact Stripe request uses the same key
`pro-activation:{id}`; this is recovery of existing consent, never a new purchase.
Routine polling uses GET exclusively. After 23 hours the backend stops replaying
an ambiguous mutation (`processing`, `error_code=recovery_required`), before
Stripe's minimum idempotency retention expires; an operator must establish its
outcome. Never create a replacement attempt or invoice to resolve that state.
If accepted terms change after consent was persisted, the backend also stops
submission (`processing`, `error_code=terms_changed`); establish the existing
payment outcome before arranging any new consent.
An unconfirmed trial whose live accepted price changed also returns `terms_changed`
instead of offering confirmation against stale terms. Refresh the preview only
after the subscription again matches its accepted offer; no payment was submitted.

In the final five minutes before the live scheduled trial end, confirmation of
the original plan returns processing and lets the already accepted scheduled
conversion happen. Preview or confirmation of a different plan is rejected with
409 in this window; it must not silently charge the original plan after showing
the other plan's terms. A previously confirmed alternate-plan attempt that reaches
this window without a definitive mutation outcome returns `processing` with
`error_code=trial_ending`; poll its existing state rather than retrying a charge.
This avoids racing a scheduled Stripe transition with an early-end request.

After `ready`, invalidate and **await refetch** of queries for:

- `GET /api/credits/trial` (active/converted state and preserved lifetime history).
- `GET /api/credits/subscription` (entitlement and billing state).
- `GET /api/chat/usage` (the applicable Pro or Max allowance and current consumption).

Use the generated query-key helpers for these exact endpoints; discard any old
paywall/exhaustion decision derived from their previous results. Preserve
`return_to` throughout hosted payment/authentication navigation.
`usage_reset=true` describes the completed initial invoice, not an operation
performed by this poll. Preserve the last announced `activation_id` across
navigation. Later renewal or plan-change invoices return false and no activation
ID; repeated recovery of the initial invoice must not repeat the fresh-usage claim.

## Rollout and recovery requirements

Pause new work and checkout/early-conversion admission, then drain all foreground,
queued, background, batch, and child-task work from old workers **before applying
the migration**. The migration records the policy start time: applying it while
old workers can still admit paid work would allow a later reconciliation to
exclude that work from the fresh allowance. Install the schema, roll out API,
reconciliation services and every worker/producer together, then resume admission.
Scheduled Stripe payments during this window reconcile after the new code starts.
Legacy batch callbacks without a stored accounting context fail closed, mark the
job errored, and release the original lock without billing under a new context.
Legacy FIFO entries with missing, invalid, or foreign accounting snapshots remain
queued without dispatch. Use the existing queue cancellation controls and submit
a new turn; do not fabricate a snapshot from the user's current paid allowance.
Held approvals without an owned accounting snapshot require a new request and
cannot execute against the current paid allowance.
These entries must still be drained before rollout. Mixed old/new usage writers must not account newly
admitted work after activation. Keep the
generation-aware accounting code during rollback for accounts already activated;
rolling back to readers of only the old Redis keys would lose correct enforcement.

Subscribe Stripe webhooks to `invoice.paid`, payment-success/failure events, and
subscription changes. Leave the periodic Stripe reconciliation sweep enabled.
Webhook retries, status polling, and the sweep all converge on the same database
activation identity. Recovery must never DELETE usage keys, clear an activation
row, clear the lifetime trial ledger, or call a billing mutation merely because
the allowance is exhausted. Retry a paid-but-unreconciled activation through GET
or the reconciliation function after the dependency recovers.

Before enabling the future frontend action, validate the pinned Stripe API using
test-mode trials/test clocks: exhausted and partial early conversion to Pro and Max,
scheduled conversion, Pro↔Max changes preserving consumption, nontrial signup
without a reset, SCA, declines, 100% discounts/customer credit, product-restricted
discounts when changing plans, duplicate events, and a
lost mutation response. Local tests use Stripe boundary doubles and real
disposable PostgreSQL/Redis for transaction/concurrency/accounting behavior; they
do not exercise production billing. No rollout or production data change is part
of this PR.

Stripe references: [subscription update and payment behavior](https://docs.stripe.com/api/subscriptions/update),
[invoice previews](https://docs.stripe.com/api/invoices/create_preview), and
[invoice payment relationships](https://docs.stripe.com/api/invoice-payment).
Product restrictions are described by the
[coupon object](https://docs.stripe.com/api/coupons/object?api-version=2025-02-24.acacia).

---

### Engineering Sentry Posthog Correlation (`docs/engineering/sentry-posthog-correlation.md`)

# Sentry and PostHog error correlation

The browser's Sentry `beforeSend` forwards surviving error/fatal events through
PostHog's Sentry integration. PostHog records `$exception` with `$sentry_event_id`
and a `$sentry_url` pointing to that exact event in Sentry. Sentry receives
`PostHog Person URL` and, while PostHog recording is active, a timestamped
`PostHog Recording URL` tag. Anonymous visitors use their current PostHog ID;
signed-in visitors use the identified user ID.

Both SDKs deliver independently. Mirroring runs before Sentry's transport
acknowledges the event. Sentry rate limits, quota rejection, or network failure
can leave a PostHog exception with a Sentry link that has no stored event.
PostHog delivery can also fail independently. Event counts and billed usage
can therefore differ; a correlation ID does not confirm storage in both products.

Forwarding and links require analytics consent and an initialized, opted-in
PostHog client. Consent is checked for each error. Early startup errors and
errors without consent still go to Sentry, but are not queued for later mirroring.
Sentry's existing filters run first. Failed HTTP responses the HttpClient
integration reports (mechanism `auto.http.client.*`) stay in Sentry only: they
are not page bugs, and a failing PostHog relay would otherwise be mirrored
back into PostHog. Warnings, transactions, backend errors, and
historical events are outside this bridge. PostHog failures do not stop Sentry
reporting. PostHog exception autocapture stays disabled to avoid a second capture
of the same browser error.

Disabling Sentry with `DISABLE_SENTRY=true` also stops PostHog exception mirroring.
The same applies when no Sentry DSN is configured or available through the hosted
fallback. PostHog product analytics continues independently.

The Sentry link uses the configured DSN's project in AutoGPT's Sentry organization,
including preview projects. Other organizations and self-hosted DSNs still get
PostHog tags, but no guessed Sentry URL.

## What PostHog receives

`beforeSend` first strips the event the way every Sentry event is stripped
(the user reduced to id and role, IP-like span attributes dropped), runs the
filters, and only then mirrors it. PostHog's integration copies just these
fields of the Sentry event into `$exception`:

- `$exception_list` and `$sentry_exception`: the exception type, message,
  mechanism and stack frames (file URLs, functions, lines and columns).
- `$exception_message`, `$exception_type`, `$sentry_exception_message`,
  `$sentry_exception_type` and `$exception_level`.
- `$sentry_event_id`, `$sentry_url` and `$sentry_tags` (the Sentry tags,
  including the PostHog person and recording links).

It never copies the Sentry user, request (URL, headers), contexts, extra data
or breadcrumbs; `sentry-posthog-instrumentation.test.ts` checks that no email,
IP address, IP header or breadcrumb URL reaches PostHog. PostHog then adds the
standard properties it puts on every consented event (current URL, browser,
device, session and person).

## Deploy

The bridge uses the existing public PostHog key/host, Sentry DSN, and Cookiebot
configuration. Deploying the frontend enables it for future eligible events.
The Sentry warehouse connector is independent: it imports tables, and does not
install this browser bridge or synchronize issue resolution status.

Source-map upload additionally requires these **build-time** variables:

- `POSTHOG_SOURCEMAP_UPLOAD_KEY`: a private PostHog personal API key with source-map
  write access. Not the backend's `POSTHOG_API_KEY`, which is the public project key.
- `POSTHOG_SOURCEMAP_PROJECT_ID`: the destination project ID (`117338` for production).
- `NEXT_PUBLIC_POSTHOG_HOST`: the matching PostHog Cloud region.
- `NEXT_PUBLIC_SOURCEMAPS=true`: explicit opt-in for Docker builds; Vercel builds
  already enable source maps unless explicitly disabled.

Set the private key in the deployment platform's secret store, never in a public
environment variable or committed file. On Vercel, add both variables to the
Production environment only; Next.js inlines only `NEXT_PUBLIC_` variables and
`env` from `next.config.mjs` into the browser, so neither reaches the bundle.
Without them, builds still pass and PostHog shows minified stack traces. For Docker, pass it using BuildKit
`--secret id=POSTHOG_SOURCEMAP_UPLOAD_KEY,env=POSTHOG_SOURCEMAP_UPLOAD_KEY`, plus
`--build-arg POSTHOG_SOURCEMAP_PROJECT_ID=117338 --build-arg NEXT_PUBLIC_SOURCEMAPS=true`.
The Dockerfile mounts the key only for the build command. Existing Sentry upload
credentials remain necessary for Sentry's own source maps. Both plugins retain
maps, matching the existing public-source policy and avoiding deletion races.
Without PostHog upload credentials, builds continue with the existing config.

## Verify in a preview

1. Accept analytics consent, sign in, and trigger one controlled browser error
   through Sentry in the preview's project.
2. Find the `$exception` in PostHog. Follow `$sentry_url` and verify the Sentry event
   ID equals `$sentry_event_id`. Verify the stack resolves to the original source.
3. Follow the Sentry `PostHog Person URL` tag. If recording is active, follow its
   recording tag and verify the session and timestamp.
4. Repeat anonymously, then revoke analytics consent and repeat. The anonymous
   event should link to that anonymous person; the revoked event should only
   reach Sentry, without PostHog tags.
5. Verify a warning and an error matching an existing Sentry filter do not create
   PostHog exceptions. One accepted error should create at most one exception.

Mirroring adds a PostHog exception for each eligible Sentry event, subject to SDK
delivery limits. It does not reduce Sentry usage. Warehouse sync has its separate
row-based usage. The bridge reuses the existing PostHog SDK and asynchronous
transport; its synchronous work is consent checking and event/property mapping.

---

### Engineering Tests (`docs/engineering/tests.md`)

# Testing

The AutoGPT Platform uses several test frameworks at different layers:

- Backend tests use pytest.
- Frontend integration tests use Vitest, React Testing Library, and MSW. These
  are the primary frontend tests.
- End-to-end browser tests use [Playwright](https://playwright.dev/).
- Design system components use Storybook stories for visual coverage.

Run these Bash commands from the repository root after installing dependencies:

Backend tests require Docker running and the platform environment files. Run
`make -C autogpt_platform init-env` to create them with fresh local secrets.
Playwright requires the backend running; `pnpm test` builds
and starts the frontend, reusing an existing server when available.

```bash
(cd autogpt_platform/backend && poetry run test)
(cd autogpt_platform/autogpt_libs && poetry run pytest)
(cd autogpt_platform/frontend && pnpm test:unit) # no servers required
(cd autogpt_platform/frontend && pnpm test) # E2E: start the backend first
```

For the frontend's fast edit/test loop, run `pnpm test:unit:watch` from its workspace.

See the backend [testing guide](https://github.com/Significant-Gravitas/AutoGPT/blob/dev/autogpt_platform/backend/TESTING.md)
and frontend [testing guide](https://github.com/Significant-Gravitas/AutoGPT/blob/dev/autogpt_platform/frontend/TESTING.md)
for patterns, commands, and guidance on choosing integration or end-to-end
coverage.

## Before you start

Playwright tests require the backend server. `pnpm test` builds and starts the
frontend for you, or reuses an existing server. Wait for the backend to be ready
before running browser tests.

## Running the Playwright tests

To run the tests, you can use the following commands:

Running the tests without the UI, and headless:

```bash
pnpm test
```

If you want to run the tests in a UI where you can identify each locator used you can use the following command:

```bash
pnpm test-ui
```

You can also pass `--debug` to the test command to open the browsers in view mode rather than headless. This works with both the `pnpm test` and `pnpm test-ui` commands.

```bash
pnpm test --debug
```

In CI, the [full-stack workflow](https://github.com/Significant-Gravitas/AutoGPT/blob/dev/.github/workflows/platform-fullstack-ci.yml)
runs the Chromium project headlessly with `--retries=0 --trace=retain-on-failure`,
overriding the shared config's CI defaults. Its JSON validator requires every test to have exactly one successful
attempt and rejects skipped, flaky, unexpected, or missing results and top-level
errors. See the shared settings in
[playwright.config.ts](https://github.com/Significant-Gravitas/AutoGPT/blob/dev/autogpt_platform/frontend/playwright.config.ts).

Failure traces and test artifacts may contain requests, cookies, and session
state. Use isolated test accounts, never production accounts, and avoid exposing
credentials in traced requests or application output.

## Continuous integration

### Sharding

The [backend workflow](https://github.com/Significant-Gravitas/AutoGPT/blob/dev/.github/workflows/platform-backend-ci.yml)
and [frontend workflow](https://github.com/Significant-Gravitas/AutoGPT/blob/dev/.github/workflows/platform-frontend-ci.yml)
define the shard matrix. CI divides the backend and frontend integration suites into disjoint parallel
shards. Backend shards select data, copilot, and util/executor paths; the
remainder uses normal workspace discovery excluding those paths, including new
top-level test files and directories. All shards explicitly load the backend's
pytest configuration. Backend shards use real PostgreSQL, RabbitMQ, and Redis,
with an isolated database, virtual host, and Redis cluster for each shard.
Each shard pays its own service-startup cost, so adding shards also multiplies setup work.

### Caching

Dependency and browser caches are keyed to their lockfiles. Browser caches can
fall back to a previous cache for the same runner OS, then install missing versions.
Docker builds reuse unchanged layers. Full-stack cache export is limited to trusted `dev` pushes or an
explicit cache-publishing dispatch. Single-container builds use GHCR caches per
architecture instead of the repository Actions cache. Pull requests read the shared
`dev` validation cache but never write one; on a pull request a failed GHCR login
or an unreadable cache gives a cold build, not a failed job. Manual dispatches read and update a
branch-specific cache, with the shared `dev` validation cache as a read-only fallback.
The helper writes that shared tag only on a push to `dev`, but repository workflows
with package-write permission can overwrite it. Release publication therefore
builds each platform digest cold, without importing this cache, and smoke-tests
and scans the exact pushed digest before publishing the multi-platform manifest.
Validation cache exports are best-effort; failed image builds, smoke tests, and
security scans still fail validation.
The generated E2E seed-data cache is keyed to the files that determine the
seeded data, so it restores across commits that do not change them. Warm
caches may avoid repeated dependency downloads, seed generation, or cache
export, but they never bypass tests, linting, type checks, coverage collection, image builds, image
smoke tests, or security scans.

### Test reports

Every test job in the validation workflows uploads a JUnit XML or JSON report,
plus coverage artifacts where applicable; this does not describe release-only
publication jobs. The [JUnit validator](https://github.com/Significant-Gravitas/AutoGPT/blob/dev/.github/scripts/validate_junit.py)
and [Playwright validator](https://github.com/Significant-Gravitas/AutoGPT/blob/dev/.github/scripts/validate_playwright_json.py)
fail validation on missing or malformed reports,
zero discovered tests, count mismatches, failures, and errors. Frontend
integration and Playwright reports also reject skipped tests; the
[single-container appliance workflow](https://github.com/Significant-Gravitas/AutoGPT/blob/dev/.github/workflows/platform-single-container-docker.yml)
allows one exact test ID to skip when Bash 3 is unavailable. The
[unittest reporter](https://github.com/Significant-Gravitas/AutoGPT/blob/dev/.github/scripts/run_unittest_junit.py)
enforces this policy, including class, module, and subtest skips. Backend skips must match exact test IDs in
`.github/scripts/backend-allowed-skips.json`, seeded from the 136 existing skips
per Python version in run 33918906861; a new skip fails validation. Do not add an
ID merely to make CI green: investigate and review the changed skip policy.
An entirely skipped backend shard also fails.
Wrapped commands and malformed backend/frontend reports produce a synthetic
machine-readable error. Single-container validation instead uploads its original
reports and leaves `status.json` marked `validated: false` if verification fails.

### Manual dispatch

The backend, frontend, full-stack, single-container, Classic, CodeQL, and block-docs
workflows have no required manual-dispatch inputs. The separate advisory PR-overlap
workflow still requires `pr_number`; it is not a CI-status checker. Backend
dispatches can refresh a stacked PR's backend coverage with
`gh workflow run platform-backend-ci.yml --ref <branch> -f pr_number=<PR-number>`;
the optional `pr_number` only selects the Codecov upload target. Full-stack
dispatches import caches by default; set `publish_build_cache` only when a test
commit should deliberately refresh them. A manually dispatched single-container
run builds, smoke-tests, and scans the image but cannot publish it; publication
jobs are reachable only from a release event. Single-container validation also
runs on every `dev` push and release, while pull requests trigger it only when
appliance packaging inputs change.

### When CI fails

Open the failed Actions run and download its test-report and coverage artifacts
from the run summary. JUnit failures identify the test and traceback; validator
errors identify missing, malformed, or unexpected skipped results. Do not treat
a successful upload as proof the tests passed.

To reproduce a backend shard after configuring the same local services, run
`poetry run pytest -c pyproject.toml backend/copilot` from the backend workspace
(substitute the failing shard's paths). For a frontend shard, run
`pnpm test:unit --shard=1/4` from the frontend workspace, using its CI shard number.

### Debugging tests

There's a lot of different ways to debug tests.

My preferred is a mix of playwright's test editor and vscode.

No matter what you do, you should **always** double check that your locators are correct. Playwright will often "time out" and not give you the error message that the locator is incorrect because it can't find the element. You can do this via devtools on your browser and they should be visible on the elements tab when you use the inspect and select elements tools.

#### Using the playwright test editor

If you need to debug a test, you can use the below command to open the test in the playwright test editor. This is helpful if you want to see the test in the browser and see the state of the page as the test sees it and the locators it uses.

```bash
pnpm test --debug --test-name-pattern="test-name"
```

#### Using vscode

You can install the [Playwright Test for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) extension to get autocomplete for the playwright api (id: `ms-playwright.playwright`).

Installing this will enable the `Test Explorer` view in vscode which allows you to run, debug, and view all tests in the current project. Adding breakpoints to your tests and running them will automatically open the test editor with the correct context.

## Setting up for generating tests

With playwright, you can generate tests from existing recordings of user sessions. This is useful for creating tests that are more representative of how a user would interact with the application. We generally use this for checking what ids stuff will have and what needs ids to be added.

It is super annoying to continuously login so I highly recommend using a saved session for your tests.
This will save a file called `.auth/gentest-user.json` that can be loaded for all future gentests so that you don't have to login every time.

### Saving a session for gen tests to always use

```bash
pnpm gentests --save-storage .auth/gentest-user.json
```

Stop your session with `CTRL + C` after you are logged in and swap the `--save-storage` flag with `--load-storage` to load the session for all future tests.

### Loading a session for gen tests to always use

```bash
pnpm gentests --load-storage .auth/gentest-user.json
```

## How to make a new test

Tests are composed of page objects and test files.

Do not commit skipped or todo Vitest/Playwright tests. Fix the prerequisite or
ask maintainers to review an explicit policy exception; do not hide a failing
test behind an environment check.

The current Playwright config discovers `*-happy-path.spec.ts` files under
`autogpt_platform/frontend/src/playwright/`. Other filenames are not collected;
follow that naming pattern for tests intended to run in CI.

A page object is a class that contains methods for interacting with a page.

A test file is a file that contains tests for a page or a set of pages.

### Making a new Page Object

For tests, we use the [page object model](https://playwright.dev/docs/pom). This is a pattern where each page is a class that contains all the methods and locators for that page.
This is useful for keeping your tests organized and easy to read as well as ensuring that your tests only need to be updated in one place when the UI changes.

You should make a new page object (only when needing to add a new page, or **UI element** that is across multiple tests) using the following example.

We extend the `BasePage` class which contains shared methods for pages that have the common functionality like a navbar. If you add something like that (for example a sidebar) you should add it to the `BasePage` class. Otherwise, you should make a new page object.

Each page object should be in its own file and be named like `page-name.page.ts`.
A page object should contain methods that are actions that a user can do on that page. For example, clicking a button, filling out a form, etc. It should also contain the various helpful abstractions that are unique to that page. For example, the `BuildPage` has a method to connect blocks together.

This is a shortened example of a page object for the profile page:



```typescript title="frontend/src/playwright/pages/profile.page.ts"
--8<-- "autogpt_platform/frontend/src/playwright/pages/profile.page.ts:ProfilePageExample"
}
```

### Making a new Test File

For tests, we use our page objects to create tests. Each test file should be in `autogpt_platform/frontend/src/playwright/` and be named like `test-name-happy-path.spec.ts`. A test file can contain multiple tests. Each of which should be related to the same conceptual function. For example, a test file for the build page could have tests for building agents, creating inputs and outputs, and connecting blocks. If you wanted to specifically test building agents, you could make a new test called `building-agents-happy-path.spec.ts`.

Tests can inherit from one or more page objects, have pre-actions, and have post-actions, as well as many other features. You can learn more about the different features and how to use them [here](https://playwright.dev/docs/test-actions).

A good focused (`unit` or `single concept`) test will:

- Have a short name that describes what it is testing
- Have a single concept (building a agent, adding all blocks, connecting two blocks, etc.)
- Check pre-conditions, actions, and post-conditions, as well as have multiple validations along the way

A good non-focused (`integration` or `multiple concepts`) test will:

- Have a short name that describes what it is testing
- Have multiple concepts (building agents, creating-?exporting->importing->running an agent, connecting blocks in multiple ways with multiple inputs and outputs, etc.)
- Have a clear user experience that they are making sure works (for example, clicking the build button and making sure the agent is built, or clicking the export button and making sure the agent is exported and shows up in the monitoring system)
- Not focus on a single concept, but instead test the flow of the application as a whole. Remember you're not testing the pixel perfect UI, but the user experience.

A good test suite will have a healthy mix of focused and non-focused tests.

### Example focused test

This uses the current coverage fixture and builder page object. Save new cases
under `autogpt_platform/frontend/src/playwright/` using the
`*-happy-path.spec.ts` filename pattern.

```typescript title="frontend/src/playwright/building-agents-happy-path.spec.ts"
import { expect, test } from "./coverage-fixture";
import { E2E_AUTH_STATES } from "./credentials/accounts";
import { BuildPage } from "./pages/build.page";

test.use({ storageState: E2E_AUTH_STATES.builder });

test("builder saves an agent", async ({ page }) => {
  const buildPage = new BuildPage(page);
  await buildPage.createAndSaveSimpleAgent("Example Agent");

  await expect(page).toHaveURL(/flowID=/);
  expect(await buildPage.isRunButtonEnabled()).toBeTruthy();
});
```

The coverage fixture preserves coverage collection. The storage state supplies
an isolated test account, the page object encapsulates UI interactions, and the
assertions verify both navigation and the saved agent's runnable state.
See the current
[builder tests](https://github.com/Significant-Gravitas/AutoGPT/blob/dev/autogpt_platform/frontend/src/playwright/builder-happy-path.spec.ts)
for full scenarios and timeout choices.

### Passing information within a test

Keep tests independent: do not make one test consume an ID created by another.
Use local variables or fixtures to share setup within a test, and use
`testInfo.attach` for safe diagnostics. Avoid attaching credentials or session
state. See Playwright's
[fixtures guide](https://playwright.dev/docs/test-fixtures)
and [TestInfo API](https://playwright.dev/docs/api/class-testinfo).

## See Also

- [Writing Tests](https://playwright.dev/docs/writing-tests)
- [Code Generation](https://playwright.dev/docs/codegen-intro)
- [Test UI Mode](https://playwright.dev/docs/test-ui-mode)
- [Trace Viewer](https://playwright.dev/docs/trace-viewer-intro)
- [Getting Started with VSCode](https://playwright.dev/docs/getting-started-vscode)
- [Debugging Tests](https://playwright.dev/docs/debug)
- [Test Fixtures](https://playwright.dev/docs/test-fixtures)
- [Global Setup and Teardown](https://playwright.dev/docs/test-global-setup-teardown)
- [Test Parameterization](https://playwright.dev/docs/test-parameterize)
- [Test Events](https://playwright.dev/docs/events)
- [Test Components](https://playwright.dev/docs/test-components)
- [Test Sharding](https://playwright.dev/docs/test-sharding)
- [Accessibility Testing](https://playwright.dev/docs/accessibility-testing)
- [Authentication](https://playwright.dev/docs/auth)
- [Mocking](https://playwright.dev/docs/mock)
- [Mock Browser APIs](https://playwright.dev/docs/mock-browser-apis)
- [Code Generation](https://playwright.dev/docs/codegen)
- [Pages](https://playwright.dev/docs/pages)
- [Test Annotations](https://playwright.dev/docs/test-annotations)

---

### Engineering Tracking Plan (`docs/engineering/tracking-plan.md`)

# PostHog Tracking Plan

The single list of PostHog events: for every user action, one event name, one
sender and one set of properties. [Activation Metrics & Experiments](activation-metrics.md)
explains how PostHog fits next to the SQL views, Looker, LaunchDarkly and
DataFast; this page is the event contract.

PostHog is the only place for what people do in the app. DataFast gets no
in-app events (SECRT-2718): it keeps its script for visits and marketing
attribution, the `/tour` goals (including `tour_cta_click` with
`surface: marketplace` from the logged-out marketplace sidebar, which shares
the tour's tracking), the server-side `signup` goal
(`datafast-server.ts`), and the DataFast visitor and session ids that ride
along to the backend (`X-Datafast-*` headers) and into Stripe Checkout
metadata, so purchases are attributed to the channel that brought the visitor.

Event names follow the product analytics plan, *Every Second Counts* (owner:
Toran), section "What we will record". Where that plan names an event for an
action, this list uses its name; [Differences from the analytics plan](#differences-from-the-analytics-plan)
lists what is not aligned yet.

In code the names live in two modules. Browser call sites may still pass a
literal (`trackBrainDump("brain_dump_started")`), which is type-checked
against these modules, and backend funnel `data_index` keys embed the name
too (`briefing_generated:<id>`, `briefing_delivered:<id>`). A
rename therefore has to search for the old string, not just edit the modules:

| Sender | Module | Pin test |
| --- | --- | --- |
| Backend | `autogpt_platform/backend/backend/util/posthog_events.py` (`PostHogEvent`, `PlannedPostHogEvent`) | `posthog_events_test.py` |
| Browser | `autogpt_platform/frontend/src/services/analytics/posthog-events.ts` (`PostHogEvent` and its groups, `PlannedPostHogEvent`) | `__tests__/posthog-events.test.ts` |

## Rules

### Naming

- **Use the analytics plan's name.** When the plan names an event for an
  action, that is the name, even if we used to send something else.
- Otherwise the name is `object_action`, snake_case, action in the past
  tense: `signup_completed`, `checkout_started`, `paywall_viewed`.
- Chat (Autopilot) events start with `chat_`, never `copilot_`. The trial
  lifecycle is `trial_*`.
- **Rename once, then never again.** PostHog stores the raw string, so a
  rename empties every insight, funnel and cohort built on the old name, and
  past events cannot be backfilled. The live names that differed from the
  plan were renamed together in SECRT-2722 (see [Renamed](#renamed)); the
  pin tests reserve the old names and fail on any change to a live one.
- Put a new name in `PlannedPostHogEvent` when it is agreed, and move it into
  the live list in the change that starts sending it.

### Who sends it

- **Outcomes are sent by the backend**: something was created, charged, run,
  hired or delivered. The backend sees every path (web, API, copilot, Slack,
  Telegram, webhooks), cannot be blocked by an ad blocker, and is what the
  `analytics.*` SQL views count.
- **Interactions are sent by the browser**: views, clicks, dismissals and
  client-side timings that the backend never sees.
- One action has **one** event. Where two emitters describe the same action,
  the backend event survives and the other stops being sent (see
  [Removed](#removed)).

### Base properties

Every event must carry:

| Property | Value |
| --- | --- |
| `environment` | `settings.config.app_env` on the backend (`local`, `dev`, `prod`), the matching app environment in the browser. |
| `source` | Which emitter sent it: `chat_copilot` (the events `copilot/tracking.py` captures itself; `chat_message_sent` goes through `product_analytics` and is `platform`), `platform` (every other backend emitter), `web` (the browser), `website` (the marketing site, see [Marketing site](#marketing-site)). |
| `revision` | The deployed build's git sha, which Sentry reports as its release: `SENTRY_RELEASE` on the backend, the build's commit in the browser (`SENTRY_RELEASE`, else Vercel's `VERCEL_GIT_COMMIT_SHA`). Left out of a build that has none, such as a local one, and from `$ai_generation` (below). |

Every backend event goes through `capture()` in
`backend/util/posthog_client.py`, which applies `environment` and `source`
last, so a caller cannot overwrite them. `revision` is a super property of
every backend PostHog client instead (`revision_properties()`), so the flag
client's `$feature_flag_called` carries it too. AutoPilot's `$ai_generation`
is the exception: OpenRouter builds it from the request rather than through
our client, so it carries no `revision`; the build arrives as
`metadata_revision` from `trace.revision` instead. AutoPilot's `$ai_trace` and
`$ai_span` go through `capture()`, so they carry `revision`. The
browser registers all three once as PostHog super properties
(`src/services/analytics/posthog-base-properties.ts`), again after `reset()`
at logout; a Vercel preview reports `environment: preview`, while the dev
deployment, which is Vercel's `staging` custom environment rather than a
preview, reports `environment: dev`.

`source` is reserved for the emitter. The three events that used it for
something else now use `hire_started.surface`,
`expert_recommended.team_source` and `workflow_installed_on_expert.workflow_source`.

### Identity

- **`distinct_id` is the platform user id** for every backend event, the same
  id the browser passes to `posthog.identify(user.id)`, so both halves land
  on one person.
- **Before login the browser uses the first-party anonymous id**
  (`src/services/analytics/anonymous-id.ts`), passed to `posthog.init` as the
  bootstrap `distinctID`. `identify` merges it into the user at login, and
  `resetAnalyticsIdentity` mints a fresh one at logout.
- **The browser waits for analytics consent** (Cookiebot,
  `src/providers/posthog/posthog-consent.ts`). PostHog starts opted out:
  without consent, browser events are dropped, `identify` is not called and
  the anonymous id lasts only for the page load. Backend events do not
  depend on it.
- **No synthetic ids.** A backend event with no user is dropped
  (`posthog_client.capture` does it for every emitter), since a made-up id
  creates a person nobody can merge.
- **No email or name in event properties.** Identify ids only. The email and
  display name are person properties set by `identify` and nowhere else.
  (`expert_hired.name` is the expert's display name, not a person's.)

### Our own team and impersonation

Our team's clicks and an admin acting as a user would otherwise skew every
chart, funnel and experiment, so both are marked and a project filter leaves
them out (SECRT-2728):

- **`is_internal`** is a person property: true for an admin (Better Auth role
  `admin`) or an `@agpt.co` email, false for everyone else. The backend sets
  it at signup with the other account properties, and again whenever an
  internal account signs in (`POST /api/auth/user`), so someone made an admin
  later is flagged from their next sign-in. `poetry run cli
  posthog-audience-backfill` sets it on earlier accounts. Someone who stops
  being an admin keeps it until that backfill runs again.
- **`impersonated`** is an event property, true on every event an admin
  caused while acting as a user (`X-Act-As-User-Id`). The backend sets it in
  `posthog_client.capture` for every event a request sends with that header
  (`api/middleware/analytics.py`) and every event of a chat turn such an admin
  started; the AutoPilot events above carry it too. The browser stays the
  admin's while impersonating, so its events land on the admin's person; a
  PostHog `before_send` hook adds `impersonated` and `impersonated_user_id`
  to them (`src/services/analytics/posthog-request-context.ts`).
- **An admin acting as a user never changes the user's person.** The backend
  drops `$set_person_properties` while impersonated (the onboarding role, the
  checkout properties). The lifecycle sync still runs, since it mirrors the
  stored trial and Stripe state rather than the admin's action.
- **Session.** With analytics consent, and not while impersonating, the
  browser sends its PostHog session id as `X-PostHog-Session-Id` on API
  requests (`customMutator` through the proxy, and the chat's direct
  requests), and the backend adds it
  as `$session_id` to the events that request sends, so they join the
  browser's session and its replay. Events sent later by the executor, the
  scheduler or a webhook have no session.

The project's "Filter out internal and test users" setting (PostHog project
settings, Product analytics) holds the filter, applied to every insight that
has "Filter out internal and test users" on:

- person property `is_internal` is not `true`;
- event property `impersonated` is not `true`.

## What counts as a task

The `analytics.*` views (`user_task_daily`, `user_lifecycle`,
`retention_task_weekly`, `unit_economics_monthly`) define a task as a unit of
work a person asked for now:

- a top-level, non-dry-run agent run whose `triggerSource` is `manual` or
  `api` (or NULL, for rows older than the column), expert workflow runs
  included; **or**
- a `user` message in an Autopilot or expert chat whose session origin is not
  `automation` and whose kind is not `dream`.

A run the copilot started (`triggerSource = copilot`) is **not** a second
task: the chat turn that asked for it already counts. `agent_run_started` does fire
for copilot-started runs (with `trigger: copilot`), because it measures
human-initiated runs, not tasks. `user_lifecycle.agent_runs_human_total` is
its SQL twin and includes copilot runs too.

The same definition as a PostHog filter:

```sql
event IN ('agent_run_started', 'chat_message_sent')
AND coalesce(properties.via, '') != 'copilot'
```

In the insight UI: events `agent_run_started` and `chat_message_sent`,
filtered by `via` "is not" `copilot`. Chat turns carry no `via`, so
the filter must keep events where it is unset. The emitters already leave
out dry runs, sub-graph runs, schedule and webhook runs, and automation-origin
turns.

Automated work is measured separately: `schedule_fired` and `trigger_fired`.

## Status legend

| Status | Meaning |
| --- | --- |
| `live` | Sent today under this name. Keep it. |
| `add` | Not sent yet; the name is in `PlannedPostHogEvent` until the change that starts sending it. |

Events that are no longer sent are listed under [Removed](#removed), with
what replaced them.

## Acquisition

| Event | Sender | Status | Required properties | Fires when |
| --- | --- | --- | --- | --- |
| `$pageview` | browser | live | `$current_url` | A route or query string changes (`PostHogPageViewTracker`). |
| `tour_started` | browser | live | — | The public `/tour` page is opened (once per tab). |
| `tour_scenario_started` | browser | live | `scenario` | A tour scenario starts playing. |
| `tour_scenario_completed` | browser | live | `scenario` | A tour scenario reaches its end. |
| `tour_cta_clicked` | browser | live | `label` (`free-trial`, `signup`, `pricing`, `another-scenario`, `self-host`, `share`), `placement` where the CTA has one, `surface` (`tour`, `marketplace`) on sidebar-card clicks | A tour or logged-out marketplace call to action is clicked. |
| `marketing_opted_out` | browser | live | `surface` (`signup`) | Someone refuses marketing email in the app. `signup`: "opt out" is clicked in the legal line under the signup buttons (not "Undo"); nothing else is sent, since there is no user yet, so it only gives the opt-out rate. The refusal itself is stored on the user (`marketingOptOutAt`). |
| `signup_completed` | backend | live | `signup_method` (the auth provider, e.g. `email`, `google`: from the user's first Better Auth account row, where `credential` is reported as `email`, else from a Supabase token's `app_metadata.provider`; omitted when neither has it) | The user row is created (`data/user.py`), whichever request creates it. |

Sidebar CTA clicks retain the tour event names for compatibility. Filter
`surface` to distinguish the marketplace from the tour; older sidebar events
do not carry this property. For tour-only sidebar reports after deployment,
require `surface = tour`. The signup CTA uses `free-trial` in cloud mode and
`signup` on self-hosted installations, where creating an account starts no trial.

The tour funnel also goes to DataFast (`tour_start`, `tour_scenario_start`,
`tour_scenario_complete`, `tour_cta_click`); the PostHog events mirror it with
the same properties and nothing identifying in them: `/tour` is public and
pre-signup, so they land on the visitor's anonymous id. Both need analytics
consent, on `/tour` like everywhere else, but they still count differently:
PostHog holds an event until the visitor's answer arrives, while DataFast drops
any event fired before consent is known. So the DataFast tour funnel misses
mount-time steps (`tour_start`, the first `tour_scenario_start`) on a first
visit where the answer arrives only after the page loads; `tour_start` is
retried on the next tour visit in that session.

## Marketing site

The marketing site, `agpt.co` (repo `Significant-Gravitas/autogpt-marketing-site`),
sends to the same PostHog project as the app, so one funnel runs from a
website visit to a paid account (SECRT-2769). Its code is
`lib/posthog/posthog.ts` and `lib/posthog/goals.ts` there.

- **Consent.** The site asks through Cookiebot too, but Cookiebot keeps one
  answer per domain, so a visitor answers once on `agpt.co` and once on
  `platform.agpt.co`. The site downloads and starts `posthog-js` only once its
  answer allows analytics; before that nothing reaches PostHog, not even
  `/flags`.
- **Settings.** The app's: opted out by default, `localStorage+cookie`
  persistence with consent, autocapture and page leave, no session replay, and
  the same-origin relay at `/relay/pnosthog`.
- **Base properties.** `environment` (`prod`, `preview`, `local`, from
  Vercel), `source: website`, and `revision` (the commit) where the build has
  one.

| Event | Fires when |
| --- | --- |
| `$pageview` | A route or query string changes, and once more when consent arrives, so the landing page counts. |
| `$pageleave`, `$autocapture` | As in the app. |
| `section_viewed` | A section marked for DataFast's scroll goals comes on screen: as soon as any of it shows, and again after it left the screen and comes back. `section` is the part after `scroll_to_` (e.g. `faq`). |
| DataFast goals | Every DataFast goal is also sent to PostHog under an `object_action`, past-tense name, with the same parameters (`data-fast-goal-*` attributes, hyphens turned into underscores, or the properties passed from code, such as `pricing_billing_toggled.billing`). |

The goal names (`POSTHOG_GOAL_EVENTS` on the site, where a test fails for a
goal without one):

| DataFast goal | PostHog event |
| --- | --- |
| `demo_tour_click` | `demo_tour_clicked` |
| `demo_video_play` | `demo_video_played` |
| `demo_video_sound` | `demo_video_sound_toggled` |
| `get_started_click` | `get_started_clicked` |
| `github_selfhost_click` | `github_selfhost_clicked` |
| `github_stars_click` | `github_stars_clicked` |
| `hero_prompt_chip_click` | `hero_prompt_chip_clicked` |
| `hero_prompt_focus` | `hero_prompt_focused` |
| `hero_prompt_submit` | `hero_prompt_submitted` |
| `marketplace_agent_click` | `marketplace_agent_clicked` |
| `marketplace_browse_click` | `marketplace_browse_clicked` |
| `nav_product_click` | `nav_product_clicked` |
| `press_source_click` | `press_source_clicked` |
| `pricing_billing_toggle` | `pricing_billing_toggled` |
| `pricing_click` | `pricing_clicked` |
| `pricing_plan_cta_click` | `pricing_plan_cta_clicked` |
| `product_demo_interact` | `product_demo_interacted` |
| `product_video_play` | `product_video_played` |
| `see_it_in_action_click` | `see_it_in_action_clicked` |
| `signin_click` | `signin_clicked` |
| `testimonial_source_click` | `testimonial_source_clicked` |
| `usecase_cta_click` | `usecase_cta_clicked` |
| `watch_demo_click` | `watch_demo_clicked` |

These names follow the [naming rules](#naming) and are never renamed. They
live in the site's repo, not in `posthog-events.ts`.

### Hand-over to the app

The site keeps PostHog's state in the `ph_<project key>_posthog` cookie on
`.agpt.co`, which `platform.agpt.co` can read. It carries `$device_id`. The
app adopts that id as the visitor's anonymous id, so `identify()` at signup
merges the website pageviews into the new user:

- **While the app's answer is pending,** the app leaves the cookie (and its
  own stored identity) alone and runs PostHog on an in-memory id. Only an
  answer without analytics deletes them, the shared cookie included; the site
  does the same on its side. If Cookiebot asks again after PostHog started
  with an earlier grant, PostHog stops capturing (so it writes nothing until
  the visitor answers) but keeps its storage, the shared cookie included; a
  grant resumes it on the same id.
- **On a grant,** during the page or from an earlier answer, the visitor's
  anonymous id is, in order: the app's own `agpt_anonymous_id`; else the
  device id in the app's own PostHog storage; else the cookie's `$device_id`;
  else the page's in-memory id (`src/services/analytics/anonymous-id.ts`).
  Both device ids are read before PostHog rewrites them. If PostHog started
  on the in-memory id, it moves onto the adopted one without a reload
  (`src/providers/posthog/posthog-consent.ts`).
- **Returning app visitors keep their own id.** A browser that already has
  `agpt_anonymous_id` from an earlier visit with consent keeps it, so
  website visits made under another id do not merge into the account.
- **Only a new visitor or a new account takes the website's id.** The
  cookie's `$device_id` is adopted only when nobody is signed in, or the
  account was created in this browser (the `agpt_account_created` signup flag,
  as seen while signed in on that page: the ads signup conversion clears it).
  An older account signed in on a browser someone else used on agpt.co keeps
  the in-memory id, so `identify()` doesn't merge that person's website visits
  into it. Until the auth store knows the session (after its fallback
  validation; an auth error leaves it unknown for the page), the choice
  waits: PostHog stays opted out and in memory, events
  are held, and nothing is stored. The id then settles, PostHog moves onto it
  and starts, before `identify()`.
- **Logout and account switches forget the shared cookie** when analytics is
  not allowed (declined or still pending): PostHog then runs in memory, so its
  `reset()` would leave the previous person's id there for the next account.
- **The backend gets the id PostHog ends up with.** The attribution report
  (`useReportAttribution`, the `anonymous_id` and `posthog_distinct_id` on
  `UserAttribution`) waits while the answer is pending, then sends the adopted
  id on a grant, or the in-memory id (and no PostHog id) on a decline. It
  goes at once without a Cookiebot domain group, and when uc.js still hasn't
  arrived 10 seconds after the page loaded (blocked). A visitor who never
  answers is reported on the first later page where they do.

## Onboarding and activation

| Event | Sender | Status | Required properties | Fires when |
| --- | --- | --- | --- | --- |
| `onboarding_step_viewed` | browser | live | `step` (`team`, `autopilot`, `role`, `pain_points`, `connect`, `hire`, `preparing`) | A wizard step is shown (once per tab). The paywall step reports `paywall_viewed` instead. |
| `onboarding_completed` | backend | live | — | `ONBOARDING_COMPLETE` is first recorded for the user (`complete_onboarding_step`). |
| `brain_dump_started` | browser | live | — | Recording starts. |
| `brain_dump_completed` | browser | live | `input_mode`, `duration_secs` and `finalize_latency_ms` (voice) or `chars` (typed) | A dump was accepted and the wizard advances. `finalize_latency_ms` is the `finalizeBrainDump()` round trip, timed after the upload flush. |
| `brain_dump_canceled` | browser | live | — | Recording is cancelled. |
| `brain_dump_skipped` | browser | live | — | The step is skipped. |
| `brain_dump_recovery_shown` | browser | live | `parts` | A saved partial recording is offered back. |
| `brain_dump_recovery_used` | browser | live | — | The saved recording is used. |
| `brain_dump_retry` | browser | live | `attempt` | A failed finalize is retried. |
| `brain_dump_restarted` | browser | live | — | Recording starts over. |
| `brain_dump_download` | browser | live | — | The recording is downloaded after a failure. |
| `brain_dump_permission_denied` | browser | live | — | Microphone permission is refused. |
| `brain_dump_typed_fallback` | browser | live | `reason` | The user types instead of speaking. |
| `transcription_failed` | browser | live | `error_code`, `finalize_latency_ms` | Finalize returns a failure. |
| `welcome_dialog_closed` | browser | live | — | The first-landing welcome dialog closes. |
| `capability_card_viewed` | browser | live | `card_index`, `deck` | A capability card is shown. |
| `capability_cards_completed` | browser | live | `card_index`, `deck` | The deck is finished. |
| `capability_cards_skipped` | browser | live | `card_index`, `deck` | The deck is skipped. |
| `intro_path` | browser | live | `path` | The intro path (A/B) is chosen. |
| `intro_followup_sent` | browser | live | `chars` | First message after the intro card. |
| `later_dump_completed` | browser | live | — | A brain dump sent later from the composer. |
| `expert_recommended` | browser | live | `template_id`, `position`, `team_source` | A recommended expert card is shown. |
| `expert_recommendation_clicked` | browser | live | `template_id`, `position` | A recommended expert card is clicked. |
| `hire_all_clicked` | browser | live | `count` (experts this click hires), `already_hired`, `recommended`, `retry` | The hire step's primary button is clicked to hire every expert still unhired, or with `retry` the ones a batch failed to hire. |
| `hire_step_continued` | browser | live | `hired`, `recommended`, `via` (`auto` after a full batch, `continue`, `later` for "I'll add members later") | The hire step is left. |
| `tab_intro_shown` | browser | live | `tab` | A tab intro card is shown. |
| `tab_intro_cta_clicked` | browser | live | `tab`, `cta` | Its primary CTA is used. |
| `tab_intro_dismissed` | browser | live | `tab` | It is dismissed any other way. |
| `integration_connected` | backend | live | `provider`, `credential_type`, `method` | A credential is stored (OAuth, key, device code). |
| `credential_oauth_started` | backend | live | `provider` | The backend issues an OAuth login URL (`GET /api/integrations/{provider}/login`). Not Codex. |
| `credential_oauth_exchange_failed` | backend | live | `provider`, `status_code` (unset for an unexpected error), `failure_class` (`invalid_state`, `provider_unavailable`, `token_exchange`, `credential_merge`), `detail` (redacted, at most 200 characters) | `POST /api/integrations/{provider}/callback` returns an error, on any path. Not Codex. |
| `credential_card_never_rendered` | browser | live | `provider`, `failure_class` | A provider is missing from the provider map. |
| `credential_oauth_popup_blocked` | browser | live | `provider`, `failure_class` | Both the popup and the new tab were blocked. |
| `credential_oauth_flow_timed_out` | browser | live | `provider`, `failure_class` | The OAuth flow timed out. |
| `credential_scope_shortfall_blocked_selection` | browser | live | `provider`, `failure_class` | The granted scopes are narrower than required. |
| `credential_proceed_stuck_after_connect` | browser | live | `provider`, `failure_class` | A connected credential never reached the card. |

## Engagement

| Event | Sender | Status | Required properties | Fires when |
| --- | --- | --- | --- | --- |
| `agent_run_started` | backend | live | `graph_id`, `graph_exec_id`, `via` (`manual`, `api`, `copilot`), `via_ref`, `preset_id`; an expert's workflow run adds `expert_id` and `kind: workflow_run` | A person starts an agent run. |
| `chat_message_sent` | backend | live | `chat_session_id`, `origin` (`web`, or the bot platform the chat was opened from: `slack`, `discord`, ...), `kind: chat_turn`, `message_length`; `expert_id` in an expert chat | A person sends a message in an Autopilot or expert chat. |
| `agent_run_finished` | backend | live | `status` (`completed`, `failed`), `graph_id`, `graph_exec_id`, `via`, `expert_id`, `cost_cents`, `duration_seconds`, `is_subgraph_run`; `failure_reason` when failed | A run reaches COMPLETED or FAILED (sub-graph and automated runs included). A top-level expert run is `expert_id` set and `is_subgraph_run` false. Deduplicated on `graph_exec_id`, so a requeue or resume that finishes the same run again sends no second event. |
| `schedule_created` | backend | live | `schedule_id`, `target` (`agent`, `autopilot`, `expert`), `expert_id`, `cron`, `is_recurring`, `run_at`, `graph_id`, `chat_session_id` | Any schedule is registered, from any surface. |
| `chat_tool_called` | backend | live | `chat_session_id`, `tool_name`, `tool_call_id` | The copilot calls a tool. |
| `chat_outcome` | backend | live | `outcome_type` (`agent_run_success`, `schedule_created`), `chat_session_id`; `agent_run_success`: `graph_id`, `execution_id`, `library_agent_id`; `schedule_created`: `target` (`agent`: `graph_id`, `schedule_id`, `cron`, `library_agent_id`; `followup`: `schedule_id`, `target_chat_session_id`, `is_recurring`) | A moment of value in a chat: the copilot started a (non-dry) run or created a schedule for the user. `agent_run_started` / `schedule_created` still count the run or schedule itself. |
| `chat_library_check_outcome` | backend | live | `chat_session_id`, `outcome`, `matches_count`, `top_score` | The create-agent library check ends. |
| `voice_mode_started` | browser | live | `entry` | Voice mode is switched on. |
| `voice_mode_stopped` | browser | live | `turns`, `state` | Switched off by the user. |
| `voice_mode_timed_out` | browser | live | `turns`, `state` | Closed by the silence timeout. |
| `voice_turn_sent` | browser | live | `turn_index`, `transcript_chars`, `transcribe_latency_ms` | A spoken turn is sent. |
| `voice_turn_dropped` | browser | live | `reason` (`vad_misfire`, `transcribe_failed`, `filler_or_empty`, `interrupted`); `transcribe_latency_ms` with `filler_or_empty`; `turn_index` and `first_sound_latency_ms` (null when nothing played yet) with `interrupted` | A spoken turn is discarded, or with `interrupted` a sent turn's reply is cut off by switching voice mode off or leaving the page. A sent turn ends in exactly one of `voice_turn_completed` or this. |
| `voice_turn_completed` | browser | live | `turn_index`, `first_sound_latency_ms` (null when nothing played) | The mic reopens after the reply. |
| `voice_transcribe_retried` | browser | live | `turn_index` | A failed transcription is retried. |
| `voice_recording_downloaded` | browser | live | `turn_index` | The audio is downloaded instead. |
| `voice_mode_permission_denied` | browser | live | `stage` | Microphone permission is refused. |
| `voice_mode_error` | browser | live | `stage` | The VAD, synthesis or send failed. |
| `experts_section_viewed` | browser | live | — | The marketplace experts shelf renders with results. |
| `expert_profile_opened` | browser | live | `template_id` | An expert profile page opens. |
| `hire_started` | browser | live | `template_id`, `surface` (`onboarding`, `expert_page`) | A hire button is clicked, or once per expert of the hire step's batch. |
| `hire_flow_abandoned` | browser | live | `template_id`, `stage` | The hire dialog is closed before hiring. |
| `expert_hired` | backend | live | `expert_id`, `template_id`, `name`, `failed_preloads_count`, `surface` (`onboarding`, `expert_page`, `copilot`; unset for other API callers) | An expert is hired or an archived one revived, from any surface (`experts_db.hire_expert`). Sent once the hire's background setup settles (`_run_hire_setup`), so `failed_preloads_count` is known; a revival with nothing to set up sends it at once. An idempotent re-hire of an active expert does not fire. |
| `hire_failed` | backend | live | `template_id`, `failed_preloads_count` | Hiring raised. |
| `expert_thread_created` | browser | live | `expert_id` | A new expert chat is created. |
| `writing_style_added` | backend | live | `expert_id` | A writing style is saved on an expert. |
| `workflow_installed_on_expert` | backend | live | `expert_id`, `workflow_source` (`library`, `marketplace`), `library_agent_id` or `store_listing_version_id` | A workflow is attached to an expert. |
| `home_viewed` | browser | live | — | The home dashboard renders with data. |
| `home_attention_actioned` | browser | live | `kind`, `action` | A "needs you" item is approved or declined. |
| `home_team_member_clicked` | browser | live | `expert_id` | A team member row is clicked. |
| `home_suggestion_clicked` | browser | live | `position` (0-based among the rows shown), `surface` (`intro`, `home`), `remaining` (open rows after this one) | A suggested first task is clicked, on the greeting or under the Home composer. |
| `home_suggestion_dismissed` | browser | live | `position` (null for `all`), `surface`, `remaining`, `scope` (`one`, `all`) | A suggested first task is hidden, or "Hide all" is used. An Undo sends nothing. |
| `listing_added_to_library` | backend | live | `store_listing_version_id`, `graph_id`, `library_agent_id` | A marketplace agent is added to the library for the first time. Only a user adding a listing counts (the library route and the add-to-library block). Restoring an entry the user already had does not, and neither does a system install: an add inside another write's transaction, an expert's hire preloads, or attaching a marketplace workflow to an expert (that is `workflow_installed_on_expert`). |
| `listing_downloaded` | backend | live | `store_listing_version_id`, `graph_id` | A signed-in user downloads a marketplace agent file. Signed-out downloads have no user and are not sent. |

## Monetization

| Event | Sender | Status | Required properties | Fires when |
| --- | --- | --- | --- | --- |
| `paywall_viewed` | browser | live | `surface` (`onboarding`, `paywall_gate`, `billing`) | A paywall or plan picker is shown (once per tab per surface): the onboarding subscription step, the in-app paywall modal, the billing page's plan card. The pricing arm comes from PostHog's own `$feature/...` properties. |
| `plan_selected` | browser | live | `subscription_tier` (`PRO`, `MAX`, `BUSINESS` for Team), `billing_cycle`, `surface`; `pricing_variant` on `onboarding`, only when PostHog has assigned an arm (omitted while flags load and for users outside the rollout, who are not in `control`) | A plan is picked: a plan card on either paywall, or Upgrade / Downgrade on the billing page. |
| `billing_portal_opened` | browser | live | `surface` (`billing` for the plan card, `billing_payment_method` for the payment method card) | The Stripe billing portal is opened. |
| `checkout_started` | backend | live | `checkout_kind` (`subscription`, `top_up`, `trial`), `subscription_tier`, `billing_cycle`, `surface` (sent by the client; unset for the low-credit top-up dialog and the legacy `/profile/credits` page), `$feature/<experiment>` for every recorded experiment arm | A Stripe Checkout session is created and its URL returned (subscription, credit top-up or trial card setup). A paid plan changed in place opens no Checkout and sends nothing. |
| `checkout_abandoned` | browser | live | `checkout_kind`, `surface` | The user returns from Stripe Checkout without paying (the cancel URL: `subscription=cancelled`, `topup=cancel`, `trial=cancelled`, or the paywall modal's `paywall_checkout=cancelled` marker). Browser-sent: Stripe only reports the expiry a day later. A trial abandonment is sent once per tab per surface until the next trial checkout starts, so a refresh on the cancel URL does not count twice but a second real abandonment does. |
| `trial_offer_viewed` | browser | live | `trial_offer_version`, `subscription_tier`, `trial_duration_days`, `surface` | A trial offer card is shown. |
| `trial_cancel_popup_viewed` | browser | live | `days_left` (whole days of trial access left, rounded up; `1` under a day) | The "Cancellation confirmed" popup opens, once, right after a cancel that sets the trial to cancel at its end, so trial access continues until then. A reload never reopens it. |
| `trial_resume_clicked` | browser | live | `days_left` | Resume trial is clicked on a trial set to cancel at its end, in the popup or on the trial card. Sent on the click; `trial_resumed` (backend) confirms it took effect. |
| `trial_subscribe_now_clicked` | browser | live | `days_left` | Subscribe now is clicked in the cancellation popup. It opens Billing. There, the trial's own plan converts the trial at once on the saved card (`trial_converted`, no `checkout_started`); any other plan, or a card that asks for authentication, goes through Stripe Checkout (`checkout_started`), which ends the trial without a `trial_ended`. |
| `trial_started` | backend | live | `trial_id`, `trial_offer_version`, `subscription_tier`, `billing_cycle`, `trial_duration_days` | The trial starts. Like every trial lifecycle event, it is sent only when its notification email is queued. |
| `trial_ending` | backend | live | as above | The reminder window opens. |
| `trial_canceled` | backend | live | as above | The trial is set to cancel at its end: the cancellation is scheduled, trial access continues until the end date, and the card is not charged. |
| `trial_resumed` | backend | live | as above | A trial set to cancel is resumed before its end date, so it converts to paid at its end as first agreed. |
| `payment_failed` | backend | live | as above | The conversion charge fails. |
| `trial_converted` | backend | live | as above | The trial converts to paid. |
| `trial_ended` | backend | live | as above | The trial ends without converting. Not sent when the trial subscription ends because another plan was bought while the trial was set to cancel; that plan's `checkout_started` and `payment_succeeded` cover it. |
| `subscription_changed` | backend | live | `change_type: upgrade`, `previous_subscription_tier`, `subscription_tier`, `billing_cycle` | A paid tier upgrade takes effect. Downgrades and cancellations are not sent here yet. |
| `payment_succeeded` | backend | live | `subscription_tier`, `billing_cycle`, `amount_cents` (the invoice's `amount_paid`), `currency` | A subscription invoice is paid. Deduplicated on the invoice id: Stripe delivers both `invoice.payment_succeeded` and `invoice_payment.paid` for one invoice, and redelivers either, so revenue is counted once. |
| `topup_completed` | backend | live | `amount_credits`, `top_up_type`, `amount_cents` (the Checkout total or the auto top-up charge), `currency` | Credits are bought. |
| `subscription_cancellation_scheduled` | backend | live | `subscription_tier` | A paid plan is set to cancel at period end. |
| `subscription_ended` | backend | live | `subscription_tier`, `billing_cycle`, `reason` (Stripe `cancellation_details.reason`: `cancellation_requested`, `payment_failed`, `payment_disputed`; `payment_failed` too when our own payment-failure handler cancelled the subscription because the balance could not cover a failed renewal, which Stripe would stamp `cancellation_requested`) | A paid subscription ends (Stripe `customer.subscription.deleted`), once per subscription, alongside the ended email. A trial that ends unconverted is `trial_ended` instead; a trial ended by buying another plan sends neither. |
| `subscription_tier_reconciled` | backend | live | `direction`, `previous_subscription_tier`, `subscription_tier`, `via` | Ops signal: Stripe and the stored tier disagreed. Not a user action; keep out of funnels. |

The paywall funnel in PostHog is `paywall_viewed` → `plan_selected` →
`checkout_started` → `payment_succeeded`, with `checkout_abandoned`
as the way out.

Browser events captured before PostHog captures (a page's mount effect on a
full page load, such as the return from Stripe, runs before `posthog.init`
and before the consent opt-in) are held by
`src/services/analytics/posthog-capture.ts` while the consent answer is
unknown. With analytics granted (an earlier answer, or a region where
Cookiebot consents on its own once `uc.js` loads) they are sent, with the
time they happened, once PostHog is opted in. When analytics is declined,
there is no consent manager, or the banner is asking, they are dropped: an
answer given on the banner covers what happens after it, not what was held
before it. The once-per-tab guards on `paywall_viewed`, `checkout_abandoned`,
`onboarding_step_viewed` and `tour_started` are set only when the event is
sent or dropped this way, not while it is held.

## Retention

| Event | Sender | Status | Required properties | Fires when |
| --- | --- | --- | --- | --- |
| `schedule_fired` | backend | live | `schedule_id`, `target`, `expert_id`, `graph_id`, `graph_exec_id` or `chat_session_id` | A schedule produces work. |
| `trigger_fired` | backend | live | `webhook_id`, `graph_id`, `graph_exec_id`, `expert_id`, `preset_id`, `target` | A webhook produces a run. |
| `briefing_generated` | backend | live | `run_count`, `decision_count`, `has_content` | A morning briefing is composed (or found empty). |
| `briefing_delivered` | backend | live | `briefing_id` | It is posted to the user's thread. |
| `briefing_opened` | browser | live | — | The briefing renders on home. Not the plan's `briefing_opened_in_chat`, which is opening it in the chat thread. |
| `briefing_outcome_clicked` | browser | live | `status` | An outcome row in the briefing is clicked. |
| `expert_fired` | backend | live | `expert_id` | An expert is fired. |

Return visits are `$pageview`; account-level retention is computed in
`retention_task_weekly` and `user_lifecycle`, not sent as events.

### Email

Postmark's webhooks for the mail the platform sends itself
(`api/features/postmark/`). MailerLite's mail (the onboarding tour, the
changelog) is not covered.

| Event | Sender | Status | Required properties | Fires when |
| --- | --- | --- | --- | --- |
| `email_delivered` | backend | live | `email_type`, `message_stream`, `message_id` | The receiving mail server accepts the email (Postmark's Delivery webhook). |
| `email_opened` | backend | live | `email_type`, `message_stream`, `message_id`, `first_open` | The email's tracking pixel loads (Open webhook). Mail apps that preload images, such as Apple Mail, report opens nobody made, so read it as an upper bound. |
| `email_clicked` | backend | live | `email_type`, `message_stream`, `message_id`, `link` | A link in the email is followed (Click webhook). |
| `email_bounced` | backend | live | `email_type`, `message_stream`, `message_id`, `bounce_type` | The email bounces (Bounce webhook), soft bounces included. A hard bounce also marks the address unverified. |

- **`email_type`** is the Postmark tag the email was sent with: the
  notification type in lower case (`briefing`, `alert`, `verdict`, `ops`,
  `subscription_welcome`, `payment_failed`, `payment_final_notice`,
  `subscription_cancelled`, `subscription_resumed`, `subscription_ended`,
  `trial_update`), or `auth` for Better Auth's verification and password
  emails. Unset for mail sent before tags were added.
- **`message_stream`** is the Postmark stream (`POSTMARK_TRANSACTIONAL_STREAM`)
  and **`message_id`** Postmark's message id, which ties the events of one
  email together.
- **`link`** is the clicked link's scheme, host and path only: the query
  string and fragment are dropped, and a path segment that isn't a plain
  lower-case word, or that looks like an id or a token, becomes `:id`
  (`https://platform.agpt.co/library/agents/:id`). A link that is not
  `http(s)` keeps only its scheme (`mailto:`).
- **`bounce_type`** is Postmark's bounce type (`HardBounce`, `SoftBounce`,
  `Transient`, `SpamNotification`, ...).
- **`first_open`** is whether Postmark counts it as the first open of that
  email.
- **Who it is for.** The recipient address is looked up among users, and the
  user id is the `distinct_id`. An address with no account (such as the Ops
  inbox) sends nothing. The address is never sent or logged.
- **When.** The event carries Postmark's own time, not the time the webhook
  arrived, and is deduplicated on message id, record type and that time (and
  the link, for a click), so a webhook Postmark redelivers counts once.
- **Opens and clicks need tracking on.** Each notification email asks
  Postmark to track opens and to rewrite links in its HTML part only when
  `POSTMARK_TRACK_OPENS_AND_CLICKS` is true (off by default). Auth email is
  never tracked, since its links carry sign-in tokens. Postmark only calls the
  webhook for the record types ticked on the stream's webhook.

## LLM analytics

AutoPilot's LLM calls, tool calls and the ratings of its replies, in
PostHog's LLM analytics (SECRT-2730, SECRT-2979). All are sent server-side
for every user, whatever their cookie consent, like every backend event. The
event names are PostHog's own, not the analytics plan's.

Each AutoPilot reply is one trace, whose id is the turn id of the reply
(`$ai_trace_id`). An expert subtask or sub-session the reply spawns joins the
same trace. No event carries a tool's input or output, or an error message:
they can hold Gmail or Drive content (Google Limited Use).

| Event | Sender | Status | Required properties | Fires when |
| --- | --- | --- | --- | --- |
| `$ai_generation` | OpenRouter Broadcast | live | set by OpenRouter from the request (`backend/copilot/llm_attribution.py`): `distinct_id` = the request's `user` (the platform user id), `$ai_session_id` = its `session_id` (the chat session id), `$ai_span_name` = `trace.generation_name` (`copilot-title`, `copilot-sdk`, `copilot-baseline`), `$ai_trace_id` = `trace.trace_id` (the reply's trace: the id of the executor turn that made the call, or for a subtask the trace of the turn that spawned it, so all calls behind one reply share it; absent for a call made outside a turn), `metadata_environment` (`local`, `dev`, `prod`), `metadata_impersonated`; `metadata_revision` (the deployed build's git sha, `config.sentry_release`) when it is set; model, tokens, cost and latency from OpenRouter | An AutoPilot LLM call goes through OpenRouter: a session title, an SDK turn (main agent, sub-agents and the CLI's compaction, via `CLAUDE_CODE_EXTRA_BODY`) or a baseline turn. |
| `$ai_metric` | backend | live | `$ai_metric_name` (`user-feedback`, `copy`), `$ai_metric_value` (`1` thumbs up or copy, `0` thumbs down), `$ai_session_id` and `chat_session_id` (the chat session id), `message_id` (the reply's row id), `message_sequence`, `feedback_id` (the stored rating, shared with its Langfuse score), `impersonated`; `$ai_trace_id` when the reply row records its trace (`ChatMessage.traceId`, replies written since SECRT-2979); `$ai_feedback_text` when a thumbs-down has a comment | A reply is rated (`POST /api/chat/sessions/{id}/feedback`, `copilot/feedback.py`). Rating a reply again sends another event; the latest per `feedback_id` stands. |
| `$ai_trace` | backend | live | `$ai_trace_id` (the turn id), `$ai_span_name` = `copilot-turn`, `$ai_session_id` and `chat_session_id`, `$ai_latency` (seconds), `$ai_is_error`, `$ai_error` (the error code, when failed), `turn_id`, `outcome` (`completed`, `cancelled`, `failed`), `engine` (`sdk`, `baseline`, ...), `turn_origin` (`chat`, `scheduled`, `system`; `spawned` on a subtask's span), `tool_call_count`, `impersonated` | A reply's turn ends, if it reached its engine (`copilot/turn_tracing.py`, `copilot/ai_spans.py`). A redelivered turn sends it again under the same event uuid (dedup key = the turn id), so PostHog keeps one; likewise a subtask's span. |
| `$ai_span` | backend | live | `$ai_trace_id`, `$ai_span_id`, `$ai_parent_id`, `$ai_span_name`, `$ai_session_id` and `chat_session_id`, `$ai_latency` (seconds), `$ai_is_error`, `impersonated`. A **tool call**: `$ai_span_name` = the tool's name, `tool_call_id`, `cancelled` (platform tools) or `builtin` (the SDK CLI's own tools, sub-agents' calls included; the high-volume lookups `TodoWrite`, `Read`, `Glob`, `Grep`, `LS` and `NotebookRead` get no span, and no built-in gets one with `CHAT_POSTHOG_BUILTIN_TOOL_SPANS=false`), `$ai_is_error` true when it raised, a gate refused it or it answered with an error, but not when it stopped for an approval card or a sign-in, `$ai_error` = the exception's type name when a platform tool raised (never its message); its parent is the turn (the trace root, or the subtask's span) or the tool that called it. A built-in tool's span is parented to the turn, not to the `Task` span of the sub-agent that called it. A **subtask**: `$ai_span_name` = `copilot-subtask`, `$ai_span_id` = its turn id, parent = the spawning tool's span, which its envelope keeps, so a child that waited in the turn queue or a subtask woken by an approval stays in the trace and under the span its spawner gave it (the trace root for one queued or held before this was recorded), `depth`, plus the `$ai_trace` turn properties, with `turn_origin` = `spawned` (or `scheduled` when the subtask's own turn runs unattended) | A platform tool call ends (`BaseTool.execute`, both engines), an SDK built-in tool call ends (Pre/PostToolUse hooks), or a spawned turn ends. |

What joins them:

- **Person.** A generation's `distinct_id` and a rating's are both the
  platform user id, the id every other event and `posthog.identify` use, so
  both land on the person with their product events and replays.
- **Chat.** `$ai_session_id` is the chat session id on both, so a chat's
  generations and its ratings group in the Sessions tab and join on it.
  `message_id` says which reply was rated.
- **Reply.** `$ai_trace_id` is the reply's turn id on its generations, its
  spans, its `$ai_trace` and its ratings, so they form one trace.
  Generations sit at the trace's top level: Broadcast has no field for a
  generation's parent span, so a subtask's model calls are not nested under
  its span.
- **Environment.** OpenRouter forwards our `trace` keys as `metadata_<key>`,
  so a generation's environment is `metadata_environment`, not the
  `environment` base property; `$ai_metric`, `$ai_trace` and `$ai_span` have
  both base properties as usual. Likewise the build is `metadata_revision` on
  a generation; the backend's events set no build property of their own and
  carry only the base properties our PostHog client adds to every event.
- **Impersonation.** A turn or rating an admin made while impersonating the
  user (`X-Act-As-User-Id`) is still sent, as the user, with
  `metadata_impersonated` / `impersonated` true, so it can be filtered out.
  The mark is kept for a turn queued behind the running-turn cap and for
  sub-sessions such a turn starts. A follow-up sent while a turn runs takes
  that turn's mark, and an engine-switch continuation or an approval wake
  starts unmarked.

Calls that skip OpenRouter never reach Broadcast and have no
`$ai_generation`: direct Anthropic and Claude Code subscription deployments,
a user's connected Codex account and the local transport. Auxiliary calls
that are not attributed yet (the baseline path's context compaction, the
action gate's classifiers, web search, Graphiti memory) arrive without the
user or chat when their client goes through OpenRouter.

## Experiments

| Event | Sender | Status | Required properties | Fires when |
| --- | --- | --- | --- | --- |
| `$feature_flag_called` | browser (posthog-js) | live | set by PostHog | A PostHog flag is read (`useExperiment`). |
| `feature_flag_mismatched` | browser | live | `flag`, `launchdarkly` (`value`, `resolved`), `posthog` (`value`, `resolved`) | Ops signal: with both flag vendors configured, LaunchDarkly and PostHog resolved a flag to different values (`useDualFlag`). Not a user action; keep out of funnels. |

Arms are also stored in the database (`analytics.experiment_assignment`),
so an experiment can be read in PostHog and Looker alike.

## Person properties

The backend keeps these on the person (distinct id = platform user id) with
a `$set` event (`PostHogEvent.SET_PERSON_PROPERTIES`), sent from
`backend/data/posthog_lifecycle_sync.py` after signup, tier changes and every
Stripe subscription sync, and by a daily sweep at 04:15 UTC. The lifecycle
events above mark the moment something happens; these properties hold the
person's current state, so a cohort or breakdown can filter on them without
replaying events.

| Property | Value |
| --- | --- |
| `subscription_status` | One of the values below. Worked out from the current trial and Stripe state, never from the last event received. |
| `signup_at` | When the user row was created. |
| `trial_started_at` | When the trial started. |
| `subscription_started_at` | When the person first became a paying subscriber: the earliest start of any paid subscription, kept across a resubscribe after a gap. For a converted trial, the conversion (its first successful charge), not the trial start. |
| `subscription_canceled_at` | When the current or last paid subscription was canceled, while `subscription_canceled` or `subscription_ended`. |
| `subscription_ended_at` | When it ended, while `subscription_ended`. |

Dates are ISO-8601 UTC strings of the lifecycle moment, not of the sync. A
date that doesn't apply to the current status is `$unset`, never sent as
null.

`subscription_status` counts only **paying** subscribers as `subscribed`
(SECRT-2795). First match wins:

| Value | Meaning |
| --- | --- |
| `subscription_canceled` | A live paid subscription set to cancel; active until the period ends. |
| `subscribed` | A live (`active`) paid subscription. |
| `trial_canceled` | A running trial set to cancel (keeps access until its end, back to `in_trial` on resume), or a trial that ended unpaid because the user canceled it. |
| `in_trial` | A running trial, ours or a Stripe-native one; includes the hour between the trial's end and its first charge. |
| `payment_failed` | The latest paid subscription is `past_due` or `unpaid`, or a trial's first charge failed and Stripe is still retrying it. Access is lost meanwhile. |
| `subscription_ended` | Has paid before, and no paid subscription is live. |
| `trial_expired` | A trial that ended without a payment and without the user canceling it: it ran out, or its first charge never cleared and Stripe gave up. |
| `signed` | Anything else, including a signup that never started a trial, and a former payer now on a free (fully discounted) subscription, who keeps `subscription_started_at`. |

- **Paid** means the subscription charges something. A subscription whose
  discounts take the whole price (a 100% coupon, or an amount off at least the
  price, on the subscription or on the customer) is not paid: it never makes
  anyone `subscribed`, `subscription_canceled` or `subscription_ended`, and
  its start is not a paid start. Neither is one whose only invoice so far
  charged nothing (a 100% coupon for the first period, which Stripe takes off
  the subscription once that invoice is made). A partial discount is paid. A
  full discount that ends (a repeating coupon) makes the subscription paid
  from then on. Stacked discounts apply one after another, in Stripe's
  order. A customer's discount counts only from its start, so a coupon given
  later doesn't make an older subscription free. Someone who paid before and
  now has a live free subscription (another one, or the same one given a
  coupon after it first charged) is `signed` (not `payment_failed` or
  `subscription_ended`), keeping their first paid start.
- **The plan tier is not read.** An admin-granted plan (ENTERPRISE, BUSINESS
  or any other) with no paid Stripe subscription is `signed`; one who pays
  through Stripe is `subscribed` by the usual rule.
- **The user canceled** a trial when it was set to cancel, or canceled on
  request without a comment of ours (a comment the customer typed in the
  billing portal still counts as theirs). Our own cancels (after a failed
  payment, of a rejected trial, of a replaced plan) carry a comment starting
  `autogpt:` or `autogpt_trial:`, and Stripe giving up on the first charge
  says `payment_failed`: both are `trial_expired`.

MailerLite's `subscription_status` and its `*_date` fields hold the same
values by the same rules: both come from one mapping
(`backend/data/posthog_lifecycle.py`). Each lifecycle sync queues them for
MailerLite as well (`backend/notifications/lifecycle_audience.py`), written
only into a subscriber MailerLite already has and only when they differ. The
daily sweep is PostHog only, so nothing corrects MailerLite on a schedule: a
sync that failed before it was queued, or two syncs from different pods queued
out of order, leave it stale until the user's next sync. `poetry run cli
mailerlite-backfill --fields-only --apply --yes` (after its dry run) catches
MailerLite up. MailerLite holds days, not moments.

The role picked in the onboarding wizard is set with a `$set` of its own when
the profile is submitted at the Preparing step (`POST /api/onboarding/profile`,
`product_analytics.set_onboarding_role`), so funnels and retention can be split
by role. Someone who left the wizard before Preparing has neither property.

| Property | Value |
| --- | --- |
| `onboarding_role` | The pick, as labelled in the wizard: `Founder / CEO`, `Operations`, `Sales / BD`, `Marketing`, `Product / PM`, `Engineering`, `HR / People` or `Other`. |
| `onboarding_role_other` | What was typed after picking `Other` (trimmed, at most 100 characters); null for any other pick. |

The pick is kept on `UserOnboarding`, not read back from the AutoPilot
business understanding, whose copy AutoPilot rewrites. `poetry run cli
onboarding-role-backfill` sets both for earlier accounts with `$set_once`, from
exact picks only: any other stored value may be `Other`'s text or a rewrite,
so it is left unset. MailerLite gets the same values as `role` and
`role_other`.

The fields GTM segments checkout openers on in MailerLite are on the person
too (SECRT-2895, `backend/notifications/posthog_audience.py`). They are worked
out by the same code (`audience_enrichment.checkout_fields`) and named as in
MailerLite, except the checkout open, which is a moment here and a day there.
Every account gets `email_type`, `signin_method` and `is_internal` (PostHog
only) at signup. Every checkout opener gets the MailerLite fields from the
checkout routes and the completed-checkout webhook. Unlike
MailerLite, PostHog also gets accounts that opted out of marketing or that a
signal places in Iran or Russia: it is analytics, not marketing.

| Property | Value |
| --- | --- |
| `email_type` | `business`, `personal` (a webmail provider) or `education`. |
| `is_internal` | `true` for our own team (an admin or an `@agpt.co` email), else `false`; see [Our own team and impersonation](#our-own-team-and-impersonation). Every account, not only openers. |
| `signin_method` | `google` or `email`; `google` wins for an account that has both. |
| `checkout_opened_at` | When the account first opened Stripe checkout. |
| `country` | The country's English name, from the strongest source seen (see `country_source`). |
| `country_code` | The same country as an ISO 3166-1 alpha-2 code. |
| `country_source` | `stripe` (the billing address), `ip` (the visitor's IP country at checkout) or `timezone` (the browser's). |
| `exclude_de_at` | `yes` when any signal points at Germany or Austria, else `no`. |

MailerLite reads the subscriber before each write so it never makes a value
worse; PostHog can't be read on the way, so `$set_once`, which only fills in
what the person doesn't hold yet, keeps the same rules. The first open stays.
`exclude_de_at` `yes` is set and `no` only filled in, so it never goes back to
`no`. A billing country is set, so it always wins, and an IP or timezone
country is only filled in, so it never replaces one. The one difference:
PostHog keeps the first IP or timezone country it gets, where MailerLite moves
on to a later IP country. `poetry run cli
posthog-audience-backfill` sends them for earlier accounts, by the same rules,
from Stripe's first Checkout Session (set outright, since Stripe holds every
open) and its billing country or else the browser's timezone. An IP country is
never backfilled, since it was never stored.

## Removed

No longer sent (SECRT-2722, SECRT-2723). The names stay reserved: the pin tests fail if
one comes back, because reusing it would splice a different action onto the
history PostHog already holds.

| Event | Was sent by | Read instead |
| --- | --- | --- |
| `subscription_trial_checkout_started` | browser | `checkout_started` with `checkout_kind: trial`, sent by the backend once the trial's Checkout session exists; `surface` is the same `onboarding` / `billing`. `trial_offer_version` is dropped; `trial_started` carries it. |
| `hire_completed` | backend | `expert_hired` (same hire; it now also skips idempotent re-hires). |
| `hire_flow_completed` | browser | `expert_hired` with `surface: expert_page`. `elapsed_ms` is PostHog's time to convert from `hire_started`; `voice_picked` is dropped. |
| `onboarding_expert_hired` | browser | `expert_hired` with `surface: onboarding`. The card `position` is on `expert_recommendation_clicked`. |
| `copilot_message_sent` | backend | `chat_message_sent`, which carries `message_length`. |
| `expert_run_completed` | backend | `agent_run_finished` with `expert_id` set and `is_subgraph_run: false`. |
| `finalize_latency_ms` | browser | The `finalize_latency_ms` property on `brain_dump_completed` and `transcription_failed`. |
| `voice_transcribe_latency_ms` | browser | The `transcribe_latency_ms` property on `voice_turn_sent` (and `voice_turn_dropped` with `reason: filler_or_empty`). |
| `voice_first_sound_latency_ms` | browser | The `first_sound_latency_ms` property on `voice_turn_completed`, or on `voice_turn_dropped` with `reason: interrupted` for a reply that was cut off. |
| `experiment_exposed` | browser | Nothing: `useLaunchDarklyExperiment` had no caller and was deleted. PostHog-bucketed experiments use `$feature_flag_called`. |
| `copilot_trigger_setup` | backend | Nothing: never sent (no caller). |
| `intro_card_dismissed`, `raise_door_clicked`, `intro_start_with_autopilot` | browser | Nothing: declared, never sent. |

## Renamed

Renamed once to the analytics plan's names (SECRT-2722). The old names are
reserved like removed ones; an insight on the old name must be pointed at the
new one.

| Old name | New name | Filter to keep the old meaning |
| --- | --- | --- |
| `run_agent` | `agent_run_started` | `expert_id` is not set |
| `run_expert` (`kind: workflow_run`) | `agent_run_started` | `expert_id` is set |
| `run_autopilot` | `chat_message_sent` | `expert_id` is not set |
| `run_expert` (`kind: chat_turn`) | `chat_message_sent` | `expert_id` is set |
| `agent_run_completed` | `agent_run_finished` | `status = completed` |
| `agent_run_failed` | `agent_run_finished` | `status = failed` |
| `copilot_tool_called` | `chat_tool_called` | — |
| `copilot_library_check_outcome` | `chat_library_check_outcome` | — |
| `copilot_agent_run_success` | `chat_outcome` | `outcome_type = agent_run_success`. Dry runs no longer count; `graph_name` is dropped. |
| `copilot_agent_scheduled` | `chat_outcome` | `outcome_type = schedule_created` and `target = agent`. `graph_name` and `schedule_name` are dropped. |
| `copilot_followup_scheduled` | `chat_outcome` | `outcome_type = schedule_created` and `target = followup`. `chat_session_id` is now the chat that scheduled it; the destination is `target_chat_session_id`. |
| `subscription_trial_offer_viewed` | `trial_offer_viewed` | — |
| `subscription_trial_started` | `trial_started` | — |
| `subscription_trial_ending` | `trial_ending` | — |
| `subscription_trial_canceled` | `trial_canceled` | — |
| `subscription_trial_resumed` | `trial_resumed` | — |
| `subscription_trial_converted` | `trial_converted` | — |
| `subscription_trial_ended` | `trial_ended` | — |
| `subscription_trial_payment_failed` | `payment_failed` | — (only trial conversion charges send it today) |
| `subscription_upgraded` | `subscription_changed` | `change_type = upgrade` |
| `subscription_payment_success` | `payment_succeeded` | — |
| `credit_topup_success` | `topup_completed` | — |
| `subscription_tier_reconciliation_discrepancy` | `subscription_tier_reconciled` | — |
| `feature_flag_mismatch` | `feature_flag_mismatched` | — (not in the analytics plan; renamed to the `object_action` past-tense form before it reached production) |

### Renamed properties

Renamed in the same release as the events, to the analytics plan's envelope,
so filters and breakdowns break once. Values are unchanged unless noted.

| Old property | New property | On |
| --- | --- | --- |
| `session_id` | `chat_session_id` | `chat_message_sent`, `chat_tool_called`, `chat_outcome`, `chat_library_check_outcome`, `schedule_created`, `schedule_fired` |
| `target_session_id` | `target_chat_session_id` | `chat_outcome` (`target = followup`) |
| `trigger` | `via` | `agent_run_started`, `agent_run_finished` |
| `trigger_ref` | `via_ref` | `agent_run_started` |
| `surface` (`chat`, or the bot platform) | `origin` (`web`, or the bot platform) | `chat_message_sent`. The plan's `origin` is the channel. |
| `origin` (`interactive`) | removed | `chat_message_sent`. Automation turns are not sent, so it was always `interactive`. |

## Differences from the analytics plan

Left for follow-up changes, so this list and the plan can be compared line by
line:

- **Events the plan folds into others keep their names until that change.**
  The brain-dump events (`brain_dump_*`, `transcription_failed`,
  `later_dump_completed`) and the wizard's `intro_path` and
  `hire_step_continued` become properties of `onboarding_step_viewed` /
  `_completed` / `_skipped` / `_back`. Spoken turns (`voice_turn_sent`)
  become `chat_message_sent` with `input_mode: voice`.
- **`subscription_changed` covers upgrades only.** The plan also counts
  cancellations and downgrades there; `subscription_cancellation_scheduled`
  and `subscription_ended` stay separate events.
- **`chat_outcome` has two outcome types.** Only `agent_run_success` and
  `schedule_created` have an emitter. `agent_created`, `trigger_setup`,
  `artifact_created`, `file_produced` and `answer_only` do not.
- **Plan events we do not send yet** (`signup_started`, `chat_session_started`,
  `chat_response_completed`, `chat_blocked`, `screen_viewed`,
  `screen_engaged`, `milestone_reached`, the builder and library families,
  `email_sent`, ...) come with the plan's phases, not with this list.
- **`email_delivered` instead of the plan's `email_sent`.** Postmark's
  webhook reports that the receiving server accepted the email, which is
  delivery, not sending. `email_sent` stays free for the send itself.
- **Events the plan has no name for keep their own**, e.g.
  `integration_connected`, `schedule_created`, `hire_started`,
  `billing_portal_opened`, `marketing_opted_out`, `tour_*`,
  `tab_intro_*`, `voice_*` and `credential_*`. `briefing_opened` is the
  briefing shown on home, a different action from the plan's
  `briefing_opened_in_chat`.

## Events not in the constants modules

These are sent by PostHog or a third party, not by our code, and are listed
so nobody adds a duplicate:

- `$pageleave`, `$autocapture`, `$identify` and `$feature_flag_called`:
  posthog-js (`capture_pageleave` and `autocapture` are on).
- `$ai_generation`: OpenRouter Broadcast, for every AutoPilot LLM call that
  goes through OpenRouter (see [LLM analytics](#llm-analytics)).

---

### Engineering Transactional Email Design (`docs/engineering/transactional-email-design.md`)

# Transactional email design

All platform-generated Postmark emails use the Edition 1 email design system
(2 October 2026). The renderer builds HTML and plain text from the same event
payload; Postmark receives those bodies rather than a hosted template ID.

## Inventory

| Family | Variants | Design and delivery |
| --- | --- | --- |
| Lifecycle | Welcome, payment failed, final notice, cancellation confirmed, cancellation reversed, ended (payment/cancellation) | Billing sender; contact reply-to; service footer; no unsubscribe |
| Trial | Started, ending, canceled, resumed, converted, payment failed, ended | Same billing rules; neutral Otto; one plan/payment action |
| Briefing | Daily, weekly, monthly; quiet/standard; attention/no attention | Product sender; hello reply-to; signed cadence links; one-click unsubscribe |
| Alert | Each cause, including coalesced alerts | Amber needs-you strip and primary attention card; secondary actions are text links |
| Verdict | Approved; changes requested with/without notes | State eyebrow; reviewer feedback preserved and escaped; one marketplace action |
| Ops | Refund request, refund processed | Internal amount band and compact Otto; admin action; no unsubscribe |
| Auth | Verify email, set/reset password, confirm new email | Compact layout without hero; action button and complete fallback URL; HTML and text; original subjects and origin validation |

Organization invitations have no email sender yet. SMTP/Gmail workflow blocks
send user-authored content and are not platform transactional notices. The
onboarding/changelog files remain marketing references; updating them does not
publish content or settings to MailerLite.

## Design contract

`backend/notifications/templates/_email_ui.j2` is the shared kit. It defines the
560px sheet, 48px desktop/22px phone gutters, Geist with system fallbacks, purple
`#6144DF` actions, ink `#171925`, white sheet and lavender paper. Facts stack on
phones. At 600px and below, the paper has no side padding and the sheet has
square corners; content keeps its 22px gutters. The sheet is fluid even when a
client strips the stylesheet. The card cell owns inline 22px side padding and
centers a 464px content table, with a fixed-width Outlook wrapper. Content rows
add no side padding. Fonts load through a separate link rather than an import
inside the style block. Button links own their full padded surface,
including when styles are removed. MSO-only rules transfer that padding to
table cells for Outlook without doubling it. Inline font stacks preserve
the fallback appearance in Outlook; action glyphs request text
presentation instead of emoji. Each family owns its HTML, subject/preheader,
and text templates. The preheader appears in the lede; supplied user names and
feedback stay escaped, and customer-authored punctuation is preserved.

Plan labels use a middle dot (`Pro · monthly`). Trial prices use the same
formatted amount as lifecycle emails, with a currency code retained when the
formatter has no currency symbol (`20.00 CAD / month`, `2,000 JPY / year`).

Briefing amounts already represent dollars; neither the displayed amount nor
the balance is divided again. Balance alerts use the same dollar unit. A trial
conversion shows its actual conversion date, not the original trial end date.

Payment-failure notices are queued only when the failed-payment handler reports
that customer action is required. Wallet-settled, stale, replaced and processing
invoices do not queue them. A missing retry timestamp is not proof of exhaustion:
the final notice requires a current `unpaid` subscription. The latest invoice
still requires action when Stripe marked it uncollectible after its last retry,
unless a newer plan replaced its subscription. Canceled subscriptions
use the existing plan-ended message. The final notice dates the failed event and
links to the hosted invoice when available; it never presents the invoice's
renewal date as a future pause date.

One primary button is the default. Cancellation confirmation uses a billing
text link. A Briefing without an attention item uses highlight/run-history text
links; its cadence controls are signed for that recipient. Only the welcome is
signed by a person. Its Expert CTA is selected using the recipient's
`HIRE_EXPERTS` flag; older queued messages and failed flag lookups default to
the workflow action. Secondary Marketplace copy follows the same selection.

Billing has no unsubscribe promise it cannot honor. Product emails match the
footer unsubscribe URL to both one-click headers. Auth and internal ops have no
unsubscribe. All include the registered address; auth has only the Discord
footer link and ops has no footer links.

## Images and configuration

`EMAIL_ASSET_BASE_URL` defaults to `https://platform.agpt.co/email`, containing
`logo-light.png` (790x356 served at 100x45). Otto uses the same URL origin plus
`/autogpt-characters/v1.1/otto/neutral-transparent/256.png`, served at 160x160 or
80x80 for ops. Its transparent background lets the surrounding hero band show
through when a mail client recolors the message in dark mode. The original
opaque export remains available for already-delivered messages. The 256px,
512px and 1024px transparent exports come unchanged from the approved October 6
design handoff.

The logo and transparent Otto ship in the frontend public assets. A custom
asset host must serve both paths publicly over HTTPS without cookies or
authentication. Deploy the frontend/static assets before the backend renderer
so the new image URL resolves as soon as templates start using it.

Images use hosted PNG URLs, explicit dimensions, and descriptive alt text.
There are no data URIs or inline SVGs. The same text, facts and action links
remain when an email client blocks images. Historical image files are retained
because already-delivered messages can still reference their URLs; new
transactional templates do not use the retired art.

Deployment overrides still control `POSTMARK_SENDER_EMAIL`,
`BILLING_SENDER_EMAIL`, `PRODUCT_SENDER_EMAIL`, `OPS_SENDER_EMAIL`, and
`POSTMARK_TRANSACTIONAL_STREAM`. This migration preserves those From values.
`BILLING_REPLY_TO_EMAIL` defaults to `contact@agpt.co` and
`PRODUCT_REPLY_TO_EMAIL` to `hello@agpt.co`. Configure a verified named auth
sender in deployment; the repository must not invent or activate a sender.

The auth RPC remains compatible across old and new API and notification-service
versions during deployment and rollback. The client sends named JSON fields;
an older service ignores the new `text_body` field and still sends the HTML,
while the new service accepts requests without that field. Plain text is
included when both components use the new code. The auth RPC itself does not
require a particular service deployment order.

The trial payload formatting change does require ordered rollout: deploy the
frontend/static assets first, the notification service second, and the API
producer last. The new notification service accepts both the exact old and new
plan labels/prices while still rejecting changed trial terms. An older service
would consider the new producer's formatting obsolete and drop the queued
notice. For rollback, revert the producer first and drain new-format messages
before reverting the notification service. Keep the published assets available.

## Production configuration and reminder checks

The repository's deployment workflows dispatch to the infrastructure repository;
the defaults here do not prove the effective production values. Before rollout:

- Set `POSTMARK_SENDER_EMAIL` to the existing Postmark-verified auth address with
  its approved display name. Check the effective notification-service value;
  `invalid@invalid.com` is only a repository placeholder. Do not invent a new
  sending identity as part of a template deploy.
- Set `BILLING_REPLY_TO_EMAIL=contact@agpt.co` and
  `PRODUCT_REPLY_TO_EMAIL=hello@agpt.co` explicitly in deployment, and inspect
  delivered message headers. Preserve the configured billing, product and ops
  senders and the transactional Postmark stream.
- Check that the new public Otto URL returns a PNG without authentication from
  the configured asset host before deploying the notification service.
- Deploy the updated notification consumers before API producers: consumers
  accept old final-notice payloads without mislabeling their renewal date, while
  new producers can omit an unknown retry date and use the new last-attempt field.
- Keep user notifications disabled until the asset and access-behavior changes
  are deployed together. Confirm Stripe's final failed-payment action and its
  own recovery emails before enabling the application emails; validate an actual
  retry, wallet-covered invoice, unpaid subscription and canceled subscription.
  The final notice is sent only when that action marks the subscription
  `unpaid`; the invoice may be left past due or marked uncollectible.

To verify the trial-ending reminder, follow the delivery path in order:

1. In the live Stripe account, confirm the enabled platform webhook endpoint
   subscribes to `customer.subscription.trial_will_end`.
2. Check the Stripe Dashboard's **Trial ending events** setting is three days.
   Compare a recent event's creation time with its subscription's `trial_end`.
   The code only sends an ending notice inside the final four days (Stripe's
   three plus a day of slack for an early event), while the trial is active,
   the card is verified and cancellation is not scheduled. A notice dropped by
   these checks is logged at info as `Dropped trial <kind> notice for user
   <user id>: <reason>`. Events for an earlier checkout attempt or an unstarted
   trial, and a notice already sent once, return without this line.
3. Check the endpoint's delivery result and backend processing. A successful
   Stripe webhook response is not proof that an email was delivered.
4. Check PostHog for `trial_ending`, the current event name, and
   `subscription_trial_ending` for older deployments. This event records a
   successful notification enqueue, not Postmark delivery.
5. Check Postmark activity in the configured transactional stream for the
   billing sender and subject beginning `Your AutoGPT trial ends on`, then check
   the delivery result for the affected recipient.

Stripe's [trial-ending webhook setting](https://docs.stripe.com/billing/subscriptions/trials)
is separate from its [hosted reminder emails](https://docs.stripe.com/billing/subscriptions/trials/manage-trial-compliance),
which are sent seven days before trial end. Keep the hosted reminder disabled
when AutoGPT owns this message, so a seven-day trial does not receive a second
reminder at signup.

## Validation

The original migration passed 258 focused tests and a browser audit of 31
scenarios at two widths (62 renders), including images blocked, with 1,548 text
contrast checks and no failures. Those results and the screenshots below
predate the transparent Otto and email-client compatibility follow-up.

The notification design fixtures cover each lifecycle/trial branch, welcome
feature cohorts, Briefing cadences/attention, coalesced alerts, verdicts, and
refunds. Tests check frame/tokens, safe images, CTA exceptions, size, footers,
link parity and escaped feedback. Auth tests check all four actions, URL
escaping, origin restrictions, and plain-text delivery. Transport tests inspect
the actual Postmark request fields with the network client mocked.

For changes to the shared kit, render at 640px, 390px and 360px with images on
and off, with styles stripped, and with simulated dark backgrounds. Check
horizontal overflow, text and button visibility, and contrast against the
effective background.
Browser evidence is not an actual Gmail, Apple Mail or Outlook delivery test.
Before release, send staging samples to authorized test inboxes and inspect
light/dark mode, images off, plain text, button and fallback links, and signed
unsubscribe/cadence controls. Also exercise destinations signed out, with the
feature disabled, and after the relevant task is complete. No email was sent,
production configuration changed, or deployment performed by this PR.

## Review previews

The client compatibility follow-up was checked across all 32 fixture/auth
variants at 640px, 390px and 360px in five modes: normal, stylesheet removed,
images blocked, dark preference, and simulated dark backgrounds (480 layout
and asset checks). No case overflowed; the largest HTML was 16,204 bytes. The
dark simulation checks the transparent artwork against recolored backgrounds;
it does not reproduce a mail client's color algorithm or establish dark-mode
text contrast. Real inbox validation remains part of the release checklist.

Both button sizes also passed 60 browser clicks across the three widths with
normal and stripped styles, including each padded edge. Previously, all 48
edge clicks missed the link; only the 12 label-center clicks succeeded.

The comparisons below use local checked-in assets and synthetic fixture data.
Desktop remains 560px wide. Phones now use the viewport width, and the inline
fluid layout still fits when the stylesheet is removed.

| Scenario | Before follow-up | After follow-up |
| --- | --- | --- |
| Desktop, 640px |  |  |
| Phone, 390px |  |  |
| Phone, stylesheet removed |  |  |

### Original migration

These screenshots use synthetic data and the supplied production logo/Otto
assets. The before column is the original `dev` template; after is this shared
kit. Each comparison includes desktop (640px) and phone (390px) renderings.

---

### Engineering Workspace Media Architecture (`docs/engineering/workspace-media-architecture.md`)

# Workspace & Media File Architecture

This document describes the architecture for handling user files in AutoGPT Platform, covering persistent user storage (Workspace) and ephemeral media processing pipelines.

## Overview

The platform has two distinct file-handling layers:

| Layer | Purpose | Persistence | Scope |
|-------|---------|-------------|-------|
| **Workspace** | Long-term user file storage | Persistent (DB + GCS/local) | Per-user, session-scoped access |
| **Media Pipeline** | Ephemeral file processing for blocks | Temporary (local disk) | Per-execution |

## Database Models

### UserWorkspace

Represents a user's file storage space. Created on-demand (one per user).

```prisma
model UserWorkspace {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  userId    String   @unique
  Files     UserWorkspaceFile[]
}
```

**Key points:**
- One workspace per user (enforced by `@unique` on `userId`)
- Created lazily via `get_or_create_workspace()` 
- Uses upsert to handle race conditions

### UserWorkspaceFile

Represents a file stored in a user's workspace.

```prisma
model UserWorkspaceFile {
  id          String    @id @default(uuid())
  workspaceId String
  name        String    // User-visible filename
  path        String    // Virtual path (e.g., "/sessions/abc123/image.png")
  storagePath String    // Actual storage path (gcs://... or local://...)
  mimeType    String
  sizeBytes   BigInt
  checksum    String?   // SHA256 for integrity
  isDeleted   Boolean   @default(false)
  deletedAt   DateTime?
  metadata    Json      @default("{}")

  @@unique([workspaceId, path])  // Enforce unique paths within workspace
}
```

**Key points:**
- `path` is a virtual path for organizing files (not actual filesystem path)
- `storagePath` contains the actual GCS or local storage location
- Soft-delete pattern: `isDeleted` flag with `deletedAt` timestamp
- Path is modified on delete to free up the virtual path for reuse

---

## WorkspaceManager

**Location:** `backend/util/workspace.py`

High-level API for workspace file operations. Combines storage backend operations with database record management.

### Initialization

```python
from backend.util.workspace import WorkspaceManager

# Basic usage
manager = WorkspaceManager(user_id="user-123", workspace_id="ws-456")

# With session scoping (CoPilot sessions)
manager = WorkspaceManager(
    user_id="user-123",
    workspace_id="ws-456", 
    session_id="session-789"
)
```

### Session Scoping

When `session_id` is provided, files are isolated to `/sessions/{session_id}/`:

```python
# With session_id="abc123":
manager.write_file(content, "image.png")  
# → stored at /sessions/abc123/image.png

# Cross-session access is explicit:
manager.read_file("/sessions/other-session/file.txt")  # Works
```

**Why session scoping?**
- CoPilot conversations need file isolation
- Prevents file collisions between concurrent sessions
- Allows session cleanup without affecting other sessions

### Core Methods

| Method | Description |
|--------|-------------|
| `write_file(content, filename, path?, mime_type?, overwrite?)` | Write file to workspace |
| `read_file(path)` | Read file by virtual path |
| `read_file_by_id(file_id)` | Read file by ID |
| `list_files(path?, limit?, offset?, include_all_sessions?)` | List files |
| `delete_file(file_id)` | Soft-delete a file |
| `get_download_url(file_id, expires_in?)` | Get signed download URL |
| `get_file_info(file_id)` | Get file metadata |
| `get_file_info_by_path(path)` | Get file metadata by path |
| `get_file_count(path?, include_all_sessions?)` | Count files |

### Storage Backends

WorkspaceManager delegates to `WorkspaceStorageBackend`:

| Backend | When Used | Storage Path Format |
|---------|-----------|---------------------|
| `GCSWorkspaceStorage` | `private_user_data_bucket` (or the legacy bucket fallback) is configured | `gcs://bucket/workspaces/{ws_id}/{file_id}/{filename}` |
| `LocalWorkspaceStorage` | No GCS bucket configured | `local://{ws_id}/{file_id}/{filename}` |

### Public and private media

Hosted storage has two trust boundaries:

- `PRIVATE_USER_DATA_BUCKET` is the default destination for user uploads,
  generated library images, custom Expert avatars, workspaces, transcripts,
  and temporary agent inputs. Private media is read through an authenticated,
  non-cacheable API that serves a file to its owner, admins and members of
  an organization the owner belongs to.
- `PUBLIC_SITE_MEDIA_BUCKET` contains only objects that a trusted publication
  flow explicitly copied after approval, plus media whose purpose is inherently
  public such as OAuth consent-screen logos. Anonymous exact-object reads are
  allowed, but anonymous bucket listing is not.

An upload caller cannot select the public destination. Publication is a
separate privileged operation; sharing a private resource grants access through
its opaque application URL and does not make its storage prefix public.

### Moving a single-bucket deployment to split buckets

Stored rows hold `gcs://<bucket>/...` paths and full GCS URLs, and the storage
code only reads from the configured private bucket, so the existing bucket must
become the private one:

1. Create the new public bucket. Deploy with `PRIVATE_USER_DATA_BUCKET` set to
   the existing `MEDIA_GCS_BUCKET_NAME` bucket and `PUBLIC_SITE_MEDIA_BUCKET`
   set to the new one. Pointing `PRIVATE_USER_DATA_BUCKET` at a new bucket
   instead makes every existing workspace file and transcript unreadable.
   Cloud startup rejects that unsafe partial migration. After every stored
   bucket-qualified path has been migrated to a new private bucket, clear
   `MEDIA_GCS_BUCKET_NAME` before selecting the new private bucket.
   Deploy the frontend first: once the backend has both names set, uploads
   return `/api/store/submissions/media/...`, which a frontend without the new
   rewrite answers with a 404.
   If anonymous users hold `roles/storage.objectViewer` on the old bucket,
   they can list every object in it until step 4. Swap that binding for
   `roles/storage.legacyObjectReader` first: existing links keep working and
   listing stops.
2. Copy everything that is already public to the public bucket and repoint its
   rows: `poetry run python scripts/publish_live_media.py` (dry run), then
   again with `--apply` until it exits 0. A non-zero exit means a live
   reference was not published (copy failure, conflict, an object outside the
   listing's owners or an unrecognised URL) and would break in step 4. This
   covers approved listing media, the avatars of creators with a public
   listing, library copies of listing images and OAuth app logos. It is also
   the repair tool when a copy at approval time failed.
3. Rewrite the remaining private media URLs to the authenticated API path:
   `poetry run python scripts/backfill_private_media_urls.py` (dry run), then
   `--apply`, re-running until it reports no conflicts. It exits 2 while any
   reference stays on the old bucket (held as public, cross-user, ambiguous,
   malformed or unrecognised): those stop loading in step 4, so check the
   counts before going on. Both scripts commit in small batches and can be
   re-run.
4. Run step 2's dry run once more and check it exits 0. Then remove every
   public binding from the old bucket and turn on public access prevention, so
   no object-level grant can expose a file again.

The private media endpoint serves a file to its owner, to admins and to
members of an organization the owner belongs to, so a leaked URL is useless to
anyone else. Published copies are never deleted automatically: when a listing
is taken down or a creator changes their avatar, the old public copy stays in
the public bucket until someone removes it by hand.

Hosted private image uploads are limited to 4 MiB so the frontend proxy can
buffer and deliver the complete authenticated response below Vercel's body
limit. Private videos retain the general 50 MiB upload limit and are delivered
in bounded range responses. An oversized object written before this limit is
served only after the same authorization check, using a 60-second signed URL
that bypasses the frontend proxy without granting bucket listing access.

---

## store_media_file()

**Location:** `backend/util/file.py`

The media normalization pipeline. Handles various input types and normalizes them for processing or output.

### Purpose

Blocks receive files in many formats (URLs, data URIs, workspace references, local paths). `store_media_file()` normalizes these to a consistent format based on what the block needs.

### Input Types Handled

| Input Format | Example | How It's Processed |
|--------------|---------|-------------------|
| Data URI | `data:image/png;base64,iVBOR...` | Decoded, virus scanned, written locally |
| HTTP(S) URL | `https://example.com/image.png` | Downloaded, virus scanned, written locally |
| Workspace URI | `workspace://abc123` or `workspace:///path/to/file` | Read from workspace, virus scanned, written locally |
| Cloud path | `gcs://bucket/path` | Downloaded, virus scanned, written locally |
| Local path | `image.png` | Verified to exist in exec_file directory |

### Return Formats

The `return_format` parameter determines what you get back:

```python
from backend.util.file import store_media_file

# For local processing (ffmpeg, MoviePy, PIL)
local_path = await store_media_file(
    file=input_file,
    execution_context=ctx,
    return_format="for_local_processing"
)
# Returns: "image.png" (relative path in exec_file dir)

# For external APIs (Replicate, OpenAI, etc.)
data_uri = await store_media_file(
    file=input_file,
    execution_context=ctx,
    return_format="for_external_api"
)
# Returns: "data:image/png;base64,iVBOR..."

# For block output (adapts to execution context)
output = await store_media_file(
    file=input_file,
    execution_context=ctx,
    return_format="for_block_output"
)
# In CoPilot: Returns "workspace://file-id#image/png"
# In graphs:  Returns "data:image/png;base64,..."
```

### Execution Context

`store_media_file()` requires an `ExecutionContext` with:
- `graph_exec_id` - Required for temp file location
- `user_id` - Required for workspace access
- `workspace_id` - Optional; enables workspace features
- `session_id` - Optional; for session scoping in CoPilot

---

## Responsibility Boundaries

### Virus Scanning

| Component | Scans? | Notes |
|-----------|--------|-------|
| `store_media_file()` | ✅ Yes | Scans **all** content before writing to local disk |
| `WorkspaceManager.write_file()` | ✅ Yes | Scans content before persisting |

**Scanning happens at:**
1. `store_media_file()` — scans everything it downloads/decodes
2. `WorkspaceManager.write_file()` — scans before persistence

Tools like `WriteWorkspaceFileTool` don't need to scan because `WorkspaceManager.write_file()` handles it.

### Persistence

| Component | Persists To | Lifecycle |
|-----------|-------------|-----------|
| `store_media_file()` | Temp dir (`/tmp/exec_file/{exec_id}/`) | Cleaned after execution |
| `WorkspaceManager` | GCS or local storage + DB | Persistent until deleted |

**Automatic cleanup:** `clean_exec_files(graph_exec_id)` removes temp files after execution completes.

---

## Decision Tree: WorkspaceManager vs store_media_file

```text
┌─────────────────────────────────────────────────────┐
│ What do you need to do with the file?               │
└─────────────────────────────────────────────────────┘
                         │
           ┌─────────────┴─────────────┐
           ▼                           ▼
    Process in a block          Store for user access
    (ffmpeg, PIL, etc.)         (CoPilot files, uploads)
           │                           │
           ▼                           ▼
    store_media_file()           WorkspaceManager
    with appropriate             
    return_format                
           │                           
           │                           
    ┌──────┴──────┐                    
    ▼             ▼                    
 "for_local_   "for_block_
 processing"   output"
    │             │
    ▼             ▼
 Get local    Auto-saves to
 path for     workspace in
 tools        CoPilot context

Store for user access
    │
    ├── write_file() ─── Upload + persist (scans internally)
    ├── read_file() / get_download_url() ─── Retrieve
    └── list_files() / delete_file() ─── Manage
```

### Quick Reference

| Scenario | Use |
|----------|-----|
| Block needs to process a file with ffmpeg | `store_media_file(..., return_format="for_local_processing")` |
| Block needs to send file to external API | `store_media_file(..., return_format="for_external_api")` |
| Block returning a generated file | `store_media_file(..., return_format="for_block_output")` |
| API endpoint handling file upload | `WorkspaceManager.write_file()` (handles virus scanning internally) |
| API endpoint serving file download | `WorkspaceManager.get_download_url()` |
| Listing user's files | `WorkspaceManager.list_files()` |

---

## Key Files Reference

| File | Purpose |
|------|---------|
| `backend/data/workspace.py` | Database CRUD operations for UserWorkspace and UserWorkspaceFile |
| `backend/util/workspace.py` | `WorkspaceManager` class - high-level workspace API |
| `backend/util/workspace_storage.py` | Storage backends (GCS, local) and `WorkspaceStorageBackend` interface |
| `backend/util/file.py` | `store_media_file()` and media processing utilities |
| `backend/util/virus_scanner.py` | `VirusScannerService` and `scan_content_safe()` |
| `schema.prisma` | Database model definitions |

---

## Common Patterns

### Block Processing a User's File

```python
async def run(self, input_data, *, execution_context, **kwargs):
    # Normalize input to local path
    local_path = await store_media_file(
        file=input_data.video,
        execution_context=execution_context,
        return_format="for_local_processing",
    )
    
    # Process with local tools
    output_path = process_video(local_path)
    
    # Return (auto-saves to workspace in CoPilot)
    result = await store_media_file(
        file=output_path,
        execution_context=execution_context,
        return_format="for_block_output",
    )
    yield "output", result
```

### API Upload Endpoint

```python
from backend.util.virus_scanner import VirusDetectedError, VirusScanError

async def upload_file(file: UploadFile, user_id: str, workspace_id: str):
    content = await file.read()

    # write_file handles virus scanning internally
    manager = WorkspaceManager(user_id, workspace_id)
    try:
        workspace_file = await manager.write_file(
            content=content,
            filename=file.filename,
        )
    except VirusDetectedError:
        raise HTTPException(status_code=400, detail="File rejected: virus detected")
    except VirusScanError:
        raise HTTPException(status_code=503, detail="Virus scanning unavailable")
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))

    return {"file_id": workspace_file.id}
```

---

## Configuration

| Setting | Purpose | Default |
|---------|---------|---------|
| `private_user_data_bucket` | Private GCS bucket for user media, workspace storage, transcripts, and temporary uploads | Falls back to `media_gcs_bucket_name`, then local storage |
| `public_site_media_bucket` | Public GCS bucket for explicitly published marketplace media and OAuth app logos | Falls back to `media_gcs_bucket_name` for compatibility |
| `media_gcs_bucket_name` | Legacy single-bucket setting for self-hosted compatibility | None |
| `workspace_storage_dir` | Local storage directory | `{app_data}/workspaces` |
| `max_file_size_mb` | Maximum file size in MB | 100 |
| `clamav_service_enabled` | Enable virus scanning | true |
| `clamav_service_host` | ClamAV daemon host | localhost |
| `clamav_service_port` | ClamAV daemon port | 3310 |
| `clamav_max_concurrency` | Max concurrent scans to ClamAV daemon | 5 |
| `clamav_mark_failed_scans_as_clean` | If true, scan failures pass content through instead of rejecting (⚠️ security risk if ClamAV is unreachable) | false |

---

### Integrations SUMMARY (`docs/integrations/SUMMARY.md`)

# Table of contents

* [AutoGPT Blocks Overview](README.md)

## Guides

* [LLM Providers](guides/llm-providers.md)
* [Voice Providers](guides/voice-providers.md)

## Block Integrations

* [Agent Mail Attachments](block-integrations/agent_mail/attachments.md)
* [Agent Mail Drafts](block-integrations/agent_mail/drafts.md)
* [Agent Mail Inbox](block-integrations/agent_mail/inbox.md)
* [Agent Mail Lists](block-integrations/agent_mail/lists.md)
* [Agent Mail Messages](block-integrations/agent_mail/messages.md)
* [Agent Mail Pods](block-integrations/agent_mail/pods.md)
* [Agent Mail Threads](block-integrations/agent_mail/threads.md)
* [Airtable Bases](block-integrations/airtable/bases.md)
* [Airtable Records](block-integrations/airtable/records.md)
* [Airtable Schema](block-integrations/airtable/schema.md)
* [Airtable Triggers](block-integrations/airtable/triggers.md)
* [All Quiet Incident Search](block-integrations/allquiet/incident_search.md)
* [All Quiet Incidents](block-integrations/allquiet/incidents.md)
* [All Quiet On Call](block-integrations/allquiet/on_call.md)
* [All Quiet Teams](block-integrations/allquiet/teams.md)
* [All Quiet Triggers](block-integrations/allquiet/triggers.md)
* [AnySearch Extract](block-integrations/anysearch/extract.md)
* [AnySearch Parallel Search](block-integrations/anysearch/parallel_search.md)
* [AnySearch Search](block-integrations/anysearch/search.md)
* [Apollo Organization](block-integrations/apollo/organization.md)
* [Apollo People](block-integrations/apollo/people.md)
* [Apollo Person](block-integrations/apollo/person.md)
* [Ayrshare Post To Bluesky](block-integrations/ayrshare/post_to_bluesky.md)
* [Ayrshare Post To Facebook](block-integrations/ayrshare/post_to_facebook.md)
* [Ayrshare Post To GMB](block-integrations/ayrshare/post_to_gmb.md)
* [Ayrshare Post To Instagram](block-integrations/ayrshare/post_to_instagram.md)
* [Ayrshare Post To LinkedIn](block-integrations/ayrshare/post_to_linkedin.md)
* [Ayrshare Post To Pinterest](block-integrations/ayrshare/post_to_pinterest.md)
* [Ayrshare Post To Reddit](block-integrations/ayrshare/post_to_reddit.md)
* [Ayrshare Post To Snapchat](block-integrations/ayrshare/post_to_snapchat.md)
* [Ayrshare Post To Telegram](block-integrations/ayrshare/post_to_telegram.md)
* [Ayrshare Post To Threads](block-integrations/ayrshare/post_to_threads.md)
* [Ayrshare Post To TikTok](block-integrations/ayrshare/post_to_tiktok.md)
* [Ayrshare Post To X](block-integrations/ayrshare/post_to_x.md)
* [Ayrshare Post To YouTube](block-integrations/ayrshare/post_to_youtube.md)
* [Baas Bots](block-integrations/baas/bots.md)
* [Bannerbear Text Overlay](block-integrations/bannerbear/text_overlay.md)
* [Basic](block-integrations/basic.md)
* [Communication](block-integrations/communication.md)
* [Compass Triggers](block-integrations/compass/triggers.md)
* [Conductor Account](block-integrations/conductor/account.md)
* [Conductor Create Session](block-integrations/conductor/create_session.md)
* [Conductor Create Workspace](block-integrations/conductor/create_workspace.md)
* [Conductor Get Session](block-integrations/conductor/get_session.md)
* [Conductor Get Workspace](block-integrations/conductor/get_workspace.md)
* [Conductor List Workspaces](block-integrations/conductor/list_workspaces.md)
* [Conductor Manage Session](block-integrations/conductor/manage_session.md)
* [Conductor Manage Workspace](block-integrations/conductor/manage_workspace.md)
* [Conductor Routines](block-integrations/conductor/routines.md)
* [Conductor Sections](block-integrations/conductor/sections.md)
* [Conductor Send Message](block-integrations/conductor/send_message.md)
* [Data](block-integrations/data.md)
* [DataForB2B Enrich](block-integrations/dataforb2b/enrich.md)
* [DataForB2B Reasoning](block-integrations/dataforb2b/reasoning.md)
* [DataForB2B Search](block-integrations/dataforb2b/search.md)
* [DataForB2B Typeahead](block-integrations/dataforb2b/typeahead.md)
* [Dataforseo Keyword Suggestions](block-integrations/dataforseo/keyword_suggestions.md)
* [Dataforseo Related Keywords](block-integrations/dataforseo/related_keywords.md)
* [Discord Bot Blocks](block-integrations/discord/bot_blocks.md)
* [Discord OAuth Blocks](block-integrations/discord/oauth_blocks.md)
* [Enrichlayer LinkedIn](block-integrations/enrichlayer/linkedin.md)
* [Exa Answers](block-integrations/exa/answers.md)
* [Exa Code Context](block-integrations/exa/code_context.md)
* [Exa Contents](block-integrations/exa/contents.md)
* [Exa Research](block-integrations/exa/research.md)
* [Exa Search](block-integrations/exa/search.md)
* [Exa Similar](block-integrations/exa/similar.md)
* [Exa Webhook Blocks](block-integrations/exa/webhook_blocks.md)
* [Exa Websets](block-integrations/exa/websets.md)
* [Exa Websets Enrichment](block-integrations/exa/websets_enrichment.md)
* [Exa Websets Import Export](block-integrations/exa/websets_import_export.md)
* [Exa Websets Items](block-integrations/exa/websets_items.md)
* [Exa Websets Monitor](block-integrations/exa/websets_monitor.md)
* [Exa Websets Polling](block-integrations/exa/websets_polling.md)
* [Exa Websets Search](block-integrations/exa/websets_search.md)
* [Fal AI Video Generator](block-integrations/fal/ai_video_generator.md)
* [Firecrawl Crawl](block-integrations/firecrawl/crawl.md)
* [Firecrawl Extract](block-integrations/firecrawl/extract.md)
* [Firecrawl Map](block-integrations/firecrawl/map.md)
* [Firecrawl Scrape](block-integrations/firecrawl/scrape.md)
* [Firecrawl Search](block-integrations/firecrawl/search.md)
* [Generic Webhook Triggers](block-integrations/generic_webhook/triggers.md)
* [GitHub Checks](block-integrations/github/checks.md)
* [GitHub CI](block-integrations/github/ci.md)
* [GitHub Commits](block-integrations/github/commits.md)
* [GitHub Issues](block-integrations/github/issues.md)
* [GitHub Notifications](block-integrations/github/notifications.md)
* [GitHub Pull Requests](block-integrations/github/pull_requests.md)
* [GitHub Repo](block-integrations/github/repo.md)
* [GitHub Repo Branches](block-integrations/github/repo_branches.md)
* [GitHub Repo Files](block-integrations/github/repo_files.md)
* [GitHub Reviews](block-integrations/github/reviews.md)
* [GitHub Statuses](block-integrations/github/statuses.md)
* [GitHub Traffic](block-integrations/github/traffic.md)
* [GitHub Triggers](block-integrations/github/triggers.md)
* [GitHub Users](block-integrations/github/users.md)
* [Google Calendar](block-integrations/google/calendar.md)
* [Google Docs](block-integrations/google/docs.md)
* [Google Gmail](block-integrations/google/gmail.md)
* [Google Sheets](block-integrations/google/sheets.md)
* [Hacker News Items](block-integrations/hacker_news/items.md)
* [Hacker News Search](block-integrations/hacker_news/search.md)
* [Hacker News Stories](block-integrations/hacker_news/stories.md)
* [Hacker News Users](block-integrations/hacker_news/users.md)
* [HubSpot Company](block-integrations/hubspot/company.md)
* [HubSpot Contact](block-integrations/hubspot/contact.md)
* [HubSpot Engagement](block-integrations/hubspot/engagement.md)
* [Jina Chunking](block-integrations/jina/chunking.md)
* [Jina Embeddings](block-integrations/jina/embeddings.md)
* [Jina Fact Checker](block-integrations/jina/fact_checker.md)
* [Jina Search](block-integrations/jina/search.md)
* [Linear Comment](block-integrations/linear/comment.md)
* [Linear Issues](block-integrations/linear/issues.md)
* [Linear Projects](block-integrations/linear/projects.md)
* [LLM](block-integrations/llm.md)
* [Logic](block-integrations/logic.md)
* [Mcp Block](block-integrations/mcp/block.md)
* [Misc](block-integrations/misc.md)
* [Notion Append To Page](block-integrations/notion/append_to_page.md)
* [Notion Create Page](block-integrations/notion/create_page.md)
* [Notion Read Database](block-integrations/notion/read_database.md)
* [Notion Read Page](block-integrations/notion/read_page.md)
* [Notion Read Page Markdown](block-integrations/notion/read_page_markdown.md)
* [Notion Search](block-integrations/notion/search.md)
* [Notion Update Page Properties](block-integrations/notion/update_page_properties.md)
* [Nvidia Deepfake](block-integrations/nvidia/deepfake.md)
* [Replicate Flux Advanced](block-integrations/replicate/flux_advanced.md)
* [Replicate Replicate Block](block-integrations/replicate/replicate_block.md)
* [Rmfg Carts](block-integrations/rmfg/carts.md)
* [Rmfg Catalog](block-integrations/rmfg/catalog.md)
* [Rmfg Designs](block-integrations/rmfg/designs.md)
* [Rmfg Dfm](block-integrations/rmfg/dfm.md)
* [Rmfg Images](block-integrations/rmfg/images.md)
* [Rmfg Orders](block-integrations/rmfg/orders.md)
* [Rmfg Pay Cart](block-integrations/rmfg/pay_cart.md)
* [Rmfg Quotes](block-integrations/rmfg/quotes.md)
* [Rmfg Review Links](block-integrations/rmfg/review_links.md)
* [Rmfg Triggers](block-integrations/rmfg/triggers.md)
* [Search](block-integrations/search.md)
* [Slack Blocks](block-integrations/slack/blocks.md)
* [Slant3D Filament](block-integrations/slant3d/filament.md)
* [Slant3D Order](block-integrations/slant3d/order.md)
* [Slant3D Order Status](block-integrations/slant3d/order_status.md)
* [Slant3D Slicing](block-integrations/slant3d/slicing.md)
* [Slant3D Webhook](block-integrations/slant3d/webhook.md)
* [Smartlead Campaign](block-integrations/smartlead/campaign.md)
* [Stagehand Blocks](block-integrations/stagehand/blocks.md)
* [Stripe Triggers](block-integrations/stripe/triggers.md)
* [Stripe Link MPP](block-integrations/stripe_link/mpp.md)
* [Stripe Link Profile](block-integrations/stripe_link/profile.md)
* [Stripe Link Spend Request](block-integrations/stripe_link/spend_request.md)
* [System Library Operations](block-integrations/system/library_operations.md)
* [System Store Operations](block-integrations/system/store_operations.md)
* [Tavily Crawl](block-integrations/tavily/crawl.md)
* [Tavily Extract](block-integrations/tavily/extract.md)
* [Tavily Map](block-integrations/tavily/map.md)
* [Tavily Search](block-integrations/tavily/search.md)
* [Telegram Blocks](block-integrations/telegram/blocks.md)
* [Telegram Triggers](block-integrations/telegram/triggers.md)
* [Text](block-integrations/text.md)
* [Todoist Comments](block-integrations/todoist/comments.md)
* [Todoist Labels](block-integrations/todoist/labels.md)
* [Todoist Projects](block-integrations/todoist/projects.md)
* [Todoist Sections](block-integrations/todoist/sections.md)
* [Todoist Tasks](block-integrations/todoist/tasks.md)
* [Twitter Blocks](block-integrations/twitter/blocks.md)
* [Twitter Bookmark](block-integrations/twitter/bookmark.md)
* [Twitter Follows](block-integrations/twitter/follows.md)
* [Twitter Hide](block-integrations/twitter/hide.md)
* [Twitter Like](block-integrations/twitter/like.md)
* [Twitter List Follows](block-integrations/twitter/list_follows.md)
* [Twitter List Lookup](block-integrations/twitter/list_lookup.md)
* [Twitter List Members](block-integrations/twitter/list_members.md)
* [Twitter List Tweets Lookup](block-integrations/twitter/list_tweets_lookup.md)
* [Twitter Manage](block-integrations/twitter/manage.md)
* [Twitter Manage Lists](block-integrations/twitter/manage_lists.md)
* [Twitter Mutes](block-integrations/twitter/mutes.md)
* [Twitter Pinned Lists](block-integrations/twitter/pinned_lists.md)
* [Twitter Quote](block-integrations/twitter/quote.md)
* [Twitter Retweet](block-integrations/twitter/retweet.md)
* [Twitter Search Spaces](block-integrations/twitter/search_spaces.md)
* [Twitter Spaces Lookup](block-integrations/twitter/spaces_lookup.md)
* [Twitter Timeline](block-integrations/twitter/timeline.md)
* [Twitter Tweet Lookup](block-integrations/twitter/tweet_lookup.md)
* [Twitter User Lookup](block-integrations/twitter/user_lookup.md)
* [Typesafe Ask Many](block-integrations/typesafe/ask_many.md)
* [Typesafe Choice](block-integrations/typesafe/choice.md)
* [Typesafe Filter](block-integrations/typesafe/filter.md)
* [Typesafe Pick Best](block-integrations/typesafe/pick_best.md)
* [Typesafe Route](block-integrations/typesafe/route.md)
* [Typesafe Score](block-integrations/typesafe/score.md)
* [Typesafe Yes No](block-integrations/typesafe/yes_no.md)
* [Video Add Audio](block-integrations/video/add_audio.md)
* [Video Clip](block-integrations/video/clip.md)
* [Video Concat](block-integrations/video/concat.md)
* [Video Download](block-integrations/video/download.md)
* [Video Duration](block-integrations/video/duration.md)
* [Video Loop](block-integrations/video/loop.md)
* [Video Narration](block-integrations/video/narration.md)
* [Video Text Overlay](block-integrations/video/text_overlay.md)
* [Wolfram LLM API](block-integrations/wolfram/llm_api.md)
* [Zerobounce Validate Emails](block-integrations/zerobounce/validate_emails.md)

---

### Platform SUMMARY (`docs/platform/SUMMARY.md`)

# Table of contents

* [What is the AutoGPT Platform?](what-is-autogpt-platform.md)

## Using the Platform

* [Getting Started (Cloud)](getting-started-cloud.md)
* [AutoPilot](autopilot.md)
* [Agent Builder Guide](agent-builder-guide.md)
* [Agent Library](agent-library.md)
* [Marketplace](marketplace.md)
* [Scheduling & Triggers](scheduling-and-triggers.md)
* [Templates](templates.md)
* [Credits & Billing](credits-and-billing.md)
* [Integrations & Credentials](integrations-and-credentials.md)
* [Data Flow & Execution](data-flow-and-execution.md)
* [Sharing & Exporting Agents](sharing-and-exporting.md)

## Self-Hosting

* [Setting Up AutoGPT (Self-Host)](getting-started.md)
* [Run AutoGPT in One Docker Container](single-container.md)
* [AutoGPT Platform Installer](installer.md)
* [Advanced Setup](advanced_setup.md)

## Tutorials

* [Create a Basic Agent](create-basic-agent.md)
* [Edit an Agent](edit-agent.md)
* [Delete an Agent](delete-agent.md)
* [Download & Import an Agent](download-agent-from-marketplace-local.md)
* [Submit an Agent to the Marketplace](submit-agent-to-marketplace.md)

## Building Blocks

* [Agent Blocks Overview](agent-blocks.md)
* [Build your own Blocks](new_blocks.md)
* [Block SDK Guide](block-sdk-guide.md)

## Using AI Services

* [Using Google Gemini](gemini.md)
* [Using Ollama](ollama.md)
* [AutoPilot on a self-hosted LLM (no API keys)](copilot-local-llm.md)
* [Nightly skill learning](skill-learning.md)
* [Using AI/ML API](aimlapi.md)
* [Using D-ID](d_id.md)

## API & Integrations

* [API Introduction](integrating/api-guide.md)
* [OAuth & SSO](integrating/oauth-guide.md)

## Contributing

* [Managing LLM Models](contributing/managing-llm-models.md)
* [Agent Collaboration Architecture](contributing/agent-collaboration-architecture.md)

## Changelog

* [What's New](changelog/README.md)
  * [A clearer wallet, a kinder trial, and shorter replies from Otto](changelog/october-8-october-14-2026.md)
  * [A bigger roster, Clay & Rock avatars, folders in your workspace](changelog/september-24-september-30-2026.md)
  * [Meet your Experts: hire a specialist and put it to work](changelog/september-19-2026.md)
  * [A cleaner chat that shows its work, clickable answers, emails worth opening](changelog/august-21-september-4-2026.md)
  * [Paste screenshots into chat, talk through onboarding, self-host in one container](changelog/july-29-august-13-2026.md)
  * [A new sidebar, a smarter composer, and Otto on Telegram](changelog/july-15-july-22-2026.md)
  * [Organize files, share your work, connect Discord](changelog/june-18-july-9-2026.md)
  * [Smarter Automation, Fresher Design, Open for Business](changelog/may-7-june-10-2026.md)
  * [Artifacts, Briefings, and Builder Chat](changelog/april-10-may-1-2026.md)
  * [Smarter Starts, Faster Feedback](changelog/march-23-april-9-2026.md)
  * [Import workflows from other platforms and enjoy a polished marketplace](changelog/march-20-march-25-2026.md)
  * [Explore a new marketplace, track usage, and give instant feedback](changelog/march-13-march-20-2026.md)
  * [Organize your agents, get notified, and enjoy a cleaner chat](changelog/march-5-march-12-2026.md)
  * [Connect any app, share files, and let AutoPilot browse for you](changelog/february-26-march-4-2026.md)
  * [Telegram bots, agent folders, and a rebuilt builder](changelog/february-11-february-26-2026.md)
  * [Voice input, persistent files, and a smarter AI](changelog/january-29-february-11-2026.md)

---

### Platform Advanced Setup (`docs/platform/advanced_setup.md`)

# Advanced Setup

The advanced steps below are intended for people with sysadmin experience. If you are not comfortable with these steps, please refer to the [basic setup guide](../platform/getting-started.md).

## Introduction

For the advanced setup, first follow the [manual setup](getting-started.md#manual-setup) to get the server up and running. Once you have the server running, you can follow the steps below to configure the server for your specific needs. These steps apply to the Docker Compose setup; the [single-container image](single-container.md) is configured with the environment variables you pass to `docker run`.

## Configuration

### Setting config via environment variables

The server uses environment variables to store configs. Each part of the platform reads its own `.env` file inside `autogpt_platform/`:

- `backend/.env` for the backend services
- `frontend/.env` for the frontend
- `.env` for Docker Compose itself

The valid options are listed, with a comment for each, in the `.env.default` file next to each `.env`. `make init-env` creates any missing `.env` from its `.env.default` and generates the secrets that `.env.default` leaves blank:

```bash
cd autogpt_platform
make init-env
```

Then edit the `.env` files to change what you need. Do not copy `.env.default` over an existing `.env`: that erases your generated `ENCRYPTION_KEY`, and the integration credentials stored under it can no longer be read.

With Docker Compose, the `environment:` entries in `docker-compose.platform.yml` take precedence over the `.env` files. They connect the services to each other by their Compose names and set the database URLs, so change those settings in `docker-compose.platform.yml`. When you run the backend outside Docker, you can also set the environment variables directly in your shell. Refer to your operating system's documentation on how to set environment variables in the current session.

## Database selection

### PostgreSQL

We use PostgreSQL 15 with the pgvector extension as the database. Docker Compose runs it as the `db` service, keeps its data in `autogpt_platform/data/db/data`, and applies the Prisma migrations through the `migrate` service every time you run `docker compose up`.

To use your own PostgreSQL server instead:

1. Make sure the `vector` and `pg_trgm` extensions are available on it; the migrations create them.
2. Run `autogpt_platform/db/init/00-init.sql` against the database once. It creates the `platform` schema and an empty `auth.users` table that older migrations reference.
3. Point `DATABASE_URL` and `DIRECT_URL` at it, keeping `schema=platform` (in `docker-compose.platform.yml` under Docker, or in `backend/.env` when you run the backend directly), and point the frontend's `DATABASE_URL` at the same database.

The `migrate` service, or `make migrate` outside Docker, then applies the migrations.

## Redis

The platform uses Redis for caching, distributed locks, rate limits, and the streams that carry agent output to the browser.

The Docker Compose stack runs its own Redis Cluster: three `redis:7` shards on ports `17000` to `17002`, joined by a one-shot `redis-init` service. `make start-core` starts it along with the other dependencies.

### Using an external Redis cluster

The backend connects with a cluster client only, so the deployment must have cluster mode enabled. A standalone server, with cluster mode disabled, does not work.

The cluster also needs:

- **Redis 7.0 or later, or a compatible engine such as Valkey.** The backend uses sharded pub/sub (`SPUBLISH`, `SSUBSCRIBE`), which Redis 7.0 introduced, along with Streams and Lua scripts. It uses no Redis modules.
- **Shard addresses the backend can reach.** With `REDIS_USE_ANNOUNCED_ADDRESS=true`, the backend connects to each shard at the address the cluster announces, so those addresses must resolve from where the backend runs. Without it, the backend connects to every shard at `REDIS_HOST` and keeps only the announced port, which works only when every shard is reachable on that one host.
- **A private network between the backend and every shard.** The backend connects without TLS and does not send a username. It sends `REDIS_PASSWORD` when one is set, and a cluster without a password works too. Nothing on the connection is encrypted, so don't route it over a network you don't trust.

Point `REDIS_HOST` and `REDIS_PORT` at any node of the cluster, and set `REDIS_PASSWORD` if the cluster has one.

In the Compose stack, `REDIS_HOST`, `REDIS_PORT` and `REDIS_USE_ANNOUNCED_ADDRESS` are set in the `x-backend-env` block of `autogpt_platform/docker-compose.platform.yml`, which takes precedence over `backend/.env`, so change them there. `REDIS_PASSWORD` is read from `backend/.env`. The bundled shards still start, because the backend services wait for `redis-0` to be healthy.

## AutoGPT Agent Server Advanced set up

To run the backend server outside Docker with Poetry, follow [Backend Development](getting-started.md#backend-development).

---

### Platform Agent Blocks (`docs/platform/agent-blocks.md`)

# **How to Create an AI Agent as a Block in AutoGPT**

## **Overview**

This guide explains how to create a reusable agent block that can be used as a component in other agents.

<center><iframe width="560" height="315" src="https://www.youtube.com/embed/G5t5wbfomNE?si=dek4KKAPmx8DVOxm" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></center>

## **What Are Agent Blocks?**

Agent blocks are pre-configured, reusable AI workflows that can be used as components within larger automation systems. Think of them as "smart building blocks" - each agent block is itself a complete workflow that can:

- Accept specific inputs
- Process data using AI and traditional automation
- Produce defined outputs
- Be easily reused in different contexts

The power of agent blocks lies in their modularity. Once you create an agent with a specific capability (like translating text or analyzing sentiment), you can reuse it as a single block in other workflows. This means you can:

- Combine multiple agent blocks to create more complex automations
- Reuse proven workflows without rebuilding them
- Share agent blocks with other users
- Create hierarchical systems where specialized agents work together

For example, a content creation workflow might combine several agent blocks:

- A research agent block that gathers information
- A writing agent block that creates the initial draft
- An editing agent block that polishes the content
- A formatting agent block that prepares the final output

## **Creating the Base Agent**

### **Required Components**

1. Input Block
2. AI Text Generator Block
3. Output Block

### **Step-by-Step Setup**

1. **Add and Configure Blocks**
    * Add an Input Block
    * Add an AI Text Generator Block
    * Add an Output Block
2. **Connect Components**
    * Connect Input's result to AI Text Generator's Prompt
    * Connect AI Text Generator's response to Output's value
3. **Name the Components**
    * Name the Input Block: "question"
    * Name the Output Block: "answer"
4. **Save the Agent**
    * Choose a descriptive name (e.g., "Weather Agent")
    * Click Save



## **Converting to a Block**

1. **Access the Block Menu**
    * Go to the Builder interface
    * Click the Blocks menu
    * Click the agent tag or search the name of your agent
2. **Using the Agent Block**
    * Click on the agent block to add to your workflow
    * Save the new agent with a descriptive name (e.g., "Weather Agent")

## **Testing the Agent Block**

1. **Run the Agent**
    * Enter a test question (e.g., "How far is the Earth from the Moon?")
    * Click Run
2. **View Results**
    * Option 1: Check "Agent Outputs" section*
    * Option 2: Click "View More" for detailed results

*Note: if there is no output block then the "Agent Outputs" button will show up blank. You can see the output under view more or at bottom of the block.

## **Advanced Usage**

* You can make more complex agents by combining multiple agent blocks
* Chain different agents together for more sophisticated workflows

## **Note**

This is a basic example that can be expanded upon to create more complex agent blocks with additional functionality.

---

### Platform Agent Builder Guide (`docs/platform/agent-builder-guide.md`)

# Agent Builder Guide

## Overview

The Agent Builder is the visual editor where you design and build agents by connecting blocks together on a canvas. Each block is a single action — like generating text, calling an API, or processing data — and you wire them together to create automated workflows.

**URL:** [platform.agpt.co/build](https://platform.agpt.co/build)

## The Builder Interface

When you open the builder, you'll see:

- **Canvas**: The main workspace where you place and connect blocks
- **Blocks Menu**: A panel on the left-hand side where you browse and search for blocks
- **Save Button**: Save your agent with a title and description

## Working with Blocks

### Types of Blocks

Blocks fall into three categories:

| Type | Description |
|------|-------------|
| **Input Blocks** | Define what information the agent needs when it runs. These become the input fields users fill in when starting a task. Types include text inputs, file inputs, and more. |
| **Action Blocks** | Perform operations — AI text generation, image creation, API calls, data processing, and hundreds of integrations with external platforms. |
| **Output Blocks** | Define what the agent returns as its result. These become the visible output when a task completes. |

Input and output blocks define the **schema** of your agent — they determine what users see when running the agent. All other blocks inside the agent are internal and not exposed to the user at runtime.

{% hint style="info" %}
There is also a special type of input block: **Trigger Blocks**. These allow your agent to be activated by external events via webhooks rather than manual input. See [Scheduling & Triggers](scheduling-and-triggers.md) for details.
{% endhint %}

### Adding Blocks

1. Open the **Blocks menu** on the left-hand side of the builder
2. Browse categories or use the search bar to find a specific block
3. Click on a block to add it to the canvas

There are hundreds of blocks available, integrating with many platforms and services.

### Connecting Blocks

Blocks have **input pins** and **output pins**. Pins are typed — they handle specific data types like text, numbers, files, and more.

To connect blocks:

1. Click on an **output pin** of one block
2. Drag to an **input pin** of another block (or simply click the output pin, then click the input pin)
3. A **connection line** will appear between the two pins

When the agent runs, data flows along these connections. This is visually represented by a **coloured bead** that slides along the connection line from the output pin to the input pin.

### Configuring Blocks

Many blocks have settings you can configure directly on the block. For example:

- The **AI Text Generator** block lets you choose which language model to use
- **Integration blocks** may require credentials (see [Integrations & Credentials](integrations-and-credentials.md))
- Some blocks allow you to hardcode values on their input pins instead of connecting them to other blocks

If a block requires a credential you haven't connected yet, a **credential bar** will appear prompting you to add it (via OAuth, API key, or username/password depending on the service).

## Saving Your Agent

To save your agent:

- Press **Ctrl+S** (or **Cmd+S** on Mac), or
- Click the **Save button** in the builder

Saving creates or updates the agent graph version in your library. The builder also keeps a local recovery draft while you edit; a successful save clears that draft. Publishing to the marketplace is a separate action—see [Publishing to the Marketplace](marketplace.md#publishing-an-agent).

## Navigating Back to Your Library

After saving, click the **Agents** button in the navigation bar to return to your library and find your agent.

## Editing an Existing Agent

To open an existing agent in the builder:

1. Go to your **Agent Library** (click **Agents** in the nav bar)
2. Click on the agent you want to edit
3. Click the **three dots** menu (⋯) on the far right-hand side of the screen
4. Select **Edit Agent**

This will open the agent in the builder with all its existing blocks and connections.

## Error Handling

When a block fails during execution, it produces data on its **error pin**. As the agent creator, you decide how to handle errors:

- **Surface the error**: Connect the error pin to an output block so the error is returned as the agent's result
- **Handle gracefully**: Connect the error pin to other blocks that provide fallback behaviour, ensuring the agent continues working even when individual blocks encounter problems

## Tips

- **Name your input and output blocks clearly** — these names become the labels users see when running your agent
- **Test incrementally** — save and run your agent frequently as you build to catch issues early
- **Use the search** in the blocks menu to quickly find what you need among hundreds of available blocks
- **Check the pin types** — connections only work between compatible pin types

---

### Platform Agent Library (`docs/platform/agent-library.md`)

# Agent Library

## Overview

The Agent Library is your personal collection of agents. From here you can run agents, view task history, set up schedules and triggers, edit agents, and manage your collection.

**URL:** [platform.agpt.co/library](https://platform.agpt.co/library)

**Access:** Click the **Agents** button in the navigation bar.

## Library View

Your library displays all of your agents, including agents you've built yourself and agents you've added from the marketplace. Each agent shows its name and key information at a glance.

### Favorites and folders

Click the **heart icon** on an agent to add or remove it from the **Favorites** tab. Use folders to organize agents; create a folder from the library and choose **Move to folder** from an agent's menu.

## Agent Detail View

Click on any agent to open its detail view. This screen provides:

- **Tasks**: A full history of every time this agent has been executed
- **Scheduled**: A list of active schedules for this agent
- **Triggers**: Configured webhook triggers for trigger-based agents
- **Templates**: Saved task inputs and credential references for reuse

These are accessible via tabs on the left-hand side of the agent screen (e.g., `Tasks 286 | Scheduled 1 | Templates 0`).

### Agent Actions Menu

Click the **three dots** (⋯) on the far right-hand side of the agent screen to access:

| Action | Description |
|--------|-------------|
| **Edit Agent** | Opens the agent in the builder; marketplace agents are read-only until duplicated |
| **Delete Agent** | Permanently removes the agent from your library |
| **Export Agent to File** | Downloads the agent as a file you can share with others |

## Running an Agent

To run an agent manually:

1. Open the agent from your library
2. Click **New agent task**
3. Fill in the required inputs and credentials
4. Click **Start Task**

The task will begin executing immediately. You can watch the progress and view the results once it completes.

{% hint style="warning" %}
For a trigger-based agent, use the same **New agent task** dialog and click **Set up Trigger**. Trigger-based agents cannot be started manually. See [Scheduling & Triggers](scheduling-and-triggers.md) for details.
{% endhint %}

## Viewing Task Results

Every time an agent runs, it creates a **task**. To view task results:

1. Open the agent from your library
2. In the left-hand pane, browse the list of completed tasks
3. Click on a task to view its details

A task detail view shows:

- **Inputs**: The values that were provided when the task started
- **Outputs**: The results the agent produced
- **Cost**: The run's reported cost, displayed at the top of the task detail view

To share an output, click **Share results** and explicitly enable sharing; copying the authenticated task URL does not create a public link. See [Sharing & Exporting Agents](sharing-and-exporting.md#share-task-output-via-url) for visibility and revocation details.

## Importing an Agent

To import an agent from a file:

1. Go to your Agent Library
2. Click **Import**
3. Keep **AutoGPT agent** selected
4. Choose the exported JSON file, complete the requested metadata, and click **Upload**

The agent will be added to your library and can be run or edited like any other agent. This is useful for importing agents shared by other users outside of the marketplace.

## Deleting an Agent

1. Open the agent from your library
2. Click the **three dots** (⋯) on the far right
3. Select **Delete Agent**
4. Confirm the deletion when prompted

{% hint style="danger" %}
Deleting an agent is permanent and cannot be undone. Make sure you want to remove it before confirming.
{% endhint %}

---

### Platform Aimlapi (`docs/platform/aimlapi.md`)

# 🧠 Running AI/ML API with AutoGPT

Follow these steps to connect **AI/ML API** with the **AutoGPT** platform for high-performance AI text generation.

---

## ✅ Prerequisites

1. Make sure you have gone through and completed the [AutoGPT Setup Guide](https://agpt.co/docs/platform/getting-started), and AutoGPT is running locally at `http://localhost:3000`.
2. You have an **API key** from [AI/ML API](https://aimlapi.com/app/keys?utm_source=autogpt&utm_medium=github&utm_campaign=integration).

---

## ⚙️ Setup Steps

### 1. Start AutoGPT Locally

Follow the official guide:
[📖 AutoGPT Getting Started Guide](https://agpt.co/docs/platform/getting-started)

Make sure AutoGPT is running and accessible at:
[http://localhost:3000](http://localhost:3000)

> 💡 Keep AutoGPT running in a terminal or Docker throughout the session.



---

### 2. Open the Visual Builder

Open your browser and go to:
[http://localhost:3000/build](http://localhost:3000/build)

Or click **“Build”** in the navigation bar.



---

### 3. Add an AI Text Generator Block

1. Click the **"Blocks"** button on the left sidebar.



2. In the search bar, type `AI Text Generator`.
3. Drag the block into the canvas.



---

### 4. Select an AI/ML API Model

Click the AI Text Generator block to configure it.

In the **LLM Model** dropdown, select the model currently available from AI/ML API:



| Model | Model ID |
| --- | --- |
| Llama 3.3 70B Instruct Turbo | `meta-llama/Llama-3.3-70B-Instruct-Turbo` |

> ✅ This model is available via the OpenAI-compatible API from [AI/ML API](https://aimlapi.com/app/?utm_source=autogpt&utm_medium=github&utm_campaign=integration)

---

### 5. Configure the Prompt and API Key

Inside the **AI Text Generator** block:

1. Enter your prompt text in the **Prompt** field.
2. Enter your **AI/ML API Key** in the designated field.

🔐 You can get your key from:
[https://aimlapi.com/app/keys/](https://aimlapi.com/app/keys?utm_source=autogpt&utm_medium=github&utm_campaign=integration)









---

### 6. Save Your Agent

Click the **“Save”** button at the top-right of the builder interface:

1. Give your agent a name (e.g., `aimlapi_test_agent`).
2. Click **“Save Agent”** to confirm.



> 💡 Saving allows reuse, scheduling, and chaining in larger workflows.

---

### 7. Run Your Agent

From the workspace:

1. Press **“Run”** next to your saved agent.
2. The request will be sent to the selected AI/ML API model.



---

### 8. View the Output

1. Scroll to the **AI Text Generator** block.
2. Check the **Output** panel below it.
3. You can copy, export, or pass the result to further blocks.



---

## 🔄 Expand Your Agent

Now that AI/ML API is connected, expand your workflow by chaining additional blocks:

* 🔧 **Tools** – fetch URLs, call APIs, scrape data
* 🧠 **Memory** – retain context across interactions
* ⚙️ **Actions / Chains** – create full pipelines

---

🎉 You’re now generating AI responses using enterprise-grade models from **AI/ML API** in **AutoGPT**!

---

### Platform Autopilot (`docs/platform/autopilot.md`)

# AutoPilot

## Overview

AutoPilot is your AI assistant built directly into the AutoGPT Platform. It can perform supported platform actions through natural conversation — from running agents to generating images, conducting research, and building agents for you.

## Accessing AutoPilot

Open AutoPilot from **Home** or **New Task** in the main navigation, or go directly to [platform.agpt.co/copilot](https://platform.agpt.co/copilot).

## What AutoPilot Can Do

AutoPilot can use supported platform tools. Here's what it can help you with:

### Run Agents

Ask AutoPilot to run a supported agent in your [Agent Library](agent-library.md). It will request any missing inputs or credentials before executing it.

### Build Agents

Describe the workflow you want and AutoPilot will create an agent for you in the builder. You can also ask it to edit your existing agents; duplicate a read-only marketplace agent before editing it. See the [Agent Builder Guide](agent-builder-guide.md) for hands-on editing.

### Browse the Marketplace

Ask AutoPilot to find agents for a specific use case and it will search the marketplace for you. You can also browse the [Marketplace](marketplace.md) directly.

### Execute Blocks Directly

AutoPilot can run supported blocks directly without building a full agent. This means you can:

- **Conduct research** with Perplexity
- **Generate images** with the latest image models
- **Edit pictures** using AI image editing blocks
- **Generate videos** using video generation blocks
- **Run supported models** on inference services such as Replicate
- **Make custom HTTP requests** to APIs you configure
- **Write and execute code** with supported coding tools

### Manage Your Library

AutoPilot can help you manage your agent library, view task results, set up schedules, and more.

## Tips for Using AutoPilot

- **Be specific**: The more detail you provide, the better AutoPilot can assist you. Instead of "make me an agent", try "build me an agent that takes a blog topic as input, generates an outline with Claude, then writes the full article".
- **Iterate**: You can refine results by asking follow-up questions or requesting changes.
- **Explore capabilities**: If you're unsure whether AutoPilot supports something, ask it or browse the available blocks.

## AutoPilot vs. the Agent Builder

AutoPilot and the Agent Builder offer complementary ways to work on the platform. AutoPilot can create and edit reusable agents through natural language and run supported blocks directly, while the builder provides hands-on control over the graph.

| | AutoPilot | Agent Builder |
|---|-----------|---------------|
| **Who is it for** | Everyone — no technical knowledge required | Technical users with a grasp of visual programming |
| **Best for** | Natural language interaction, doing things fast, and accessing blocks directly | Hands-on visual control over exactly how an agent is wired together |
| **How it works** | Conversational — describe what you want | Visual — drag, drop, and connect blocks on a canvas |
| **Can build agents** | Yes — describe what you want and it builds the agent for you | Yes — you build the agent manually |
| **Can edit agents** | Yes — for user-created or duplicated editable agents | Yes — full visual editing |
| **Block access** | Can run supported blocks directly without building an agent | Blocks must be connected into an agent workflow |

Choose whichever approach suits you. Many users use both — AutoPilot for speed, and the builder when they want fine-grained visual control over their workflow.

---

### Platform Block Sdk Guide (`docs/platform/block-sdk-guide.md`)

# Block Creation with SDK

This guide explains how to create new blocks for the AutoGPT Platform using the SDK pattern with advanced features.

## Overview

Blocks are reusable components that perform specific tasks in AutoGPT workflows. They can integrate with external services, process data, or perform any programmatic operation.

## Basic Structure

### 1. Create Provider Configuration

First, create a `_config.py` file to configure your provider using the `ProviderBuilder`:

!!! note "Simple API key provider"
    ```python
    from backend.sdk import BlockCostType, ProviderBuilder

    my_provider = (
        ProviderBuilder("my_provider")
        .with_api_key("MY_PROVIDER_API_KEY", "My Provider API Key")
        .with_base_cost(1, BlockCostType.RUN)
        .build()
    )
    ```

For OAuth providers:

!!! note "OAuth provider configuration"
    ```python
    from backend.sdk import BlockCostType, ProviderBuilder
    from ._oauth import MyProviderOAuthHandler

    my_provider = (
        ProviderBuilder("my_provider")
        .with_oauth(
            MyProviderOAuthHandler,
            scopes=["read", "write"],
            client_id_env_var="MY_PROVIDER_CLIENT_ID",
            client_secret_env_var="MY_PROVIDER_CLIENT_SECRET",
        )
        .with_base_cost(1, BlockCostType.RUN)
        .build()
    )
    ```

### 2. Create the Block Class

Create your block file (e.g., `my_block.py`):

```
/* Detailed source-code truncated for AI context efficiency. */
```

!!! note "Input Schema Fields"
    - **`credentials`**: Use `my_provider.credentials_field()` to add provider authentication
    - **`query`**: Simple string field with description
    - **`limit`**: Integer field with validation constraints (`ge=1`, `le=100`)
    - **`advanced_option`**: Marked with `advanced=True` to hide from basic UI

!!! note "Output Schema Fields"
    - **`results`**: List of results from the block
    - **`count`**: Total count of results
    - The `error` output pin is already defined on `BlockSchemaOutput`

!!! note "Block Initialization"
    - **`id`**: Generate a unique ID using `uuid.uuid4()`
    - **`description`**: Brief description of what the block does
    - **`categories`**: Choose from `BlockCategory` enum (e.g., SEARCH, AI, PRODUCTIVITY)
    - **`input_schema` / `output_schema`**: Assign the Input and Output classes

!!! note "Run Method"
    - Implement your block logic in `process_data()` helper method
    - Use `credentials.api_key.get_secret_value()` to access the API key
    - Use `yield` to output results
    - **Do NOT wrap your logic in try/except** — the executor framework handles errors at a higher level. Raise exceptions (e.g., `BlockExecutionError`, `BlockInputError`) for user-fixable errors; let unexpected errors propagate naturally. See [Error Handling in new_blocks.md](new_blocks.md#error-handling) for details.

## Key Components Explained

### Provider Configuration

!!! tip "Simple API Key Integrations"
    For integrations that only use API key authentication, define credentials in `_auth.py` and use a small `_config.py` to register the provider description and supported auth types for the settings UI. See [API-Key-Only Provider Auth](new_blocks.md#api-key-only-provider-auth-recommended-pattern) in the main block guide for the complete pattern.

The `ProviderBuilder` is available for advanced use cases (OAuth, webhooks, cost tracking). It allows you to:
- **`.with_api_key()`**: Add API key authentication
- **`.with_oauth()`**: Add OAuth authentication
- **`.with_base_cost()`**: Set what a run costs when it uses the platform's own key for this provider (for example the one `.with_api_key()` reads from the environment); runs on a user's own key are free
- **`.with_webhook_manager()`**: Add webhook support
- **`.with_user_password()`**: Add username/password auth

### Block Schema

- **Input/Output classes**: Define the data structure using `BlockSchema`
- **SchemaField**: Define individual fields with validation
- **CredentialsMetaInput**: Special field for handling credentials

### Block Implementation

1. **Unique ID**: Generate using `uuid.uuid4()`
2. **Categories**: Choose from `BlockCategory` enum (e.g., SEARCH, AI, PRODUCTIVITY)
3. **async run()**: Main execution method that yields outputs
4. **Error handling**: Error output pin is already defined on BlockSchemaOutput

## Advanced Features

### Testing

Add test configuration to your block:

!!! note "Test Configuration"
    ```python
    def __init__(self):
        super().__init__(
            # ... other config ...
            test_input={
                "query": "test query",
                "limit": 5,
                "credentials": {
                    "provider": "my_provider",
                    "id": str(uuid.uuid4()),
                    "type": "api_key"
                }
            },
            test_output=[
                ("results", ["result1", "result2"]),
                ("count", 2)
            ],
            test_mock={
                "process_data": lambda *args, **kwargs: ["result1", "result2"]
            }
        )
    ```

### OAuth Support

Create an OAuth handler in `_oauth.py`:

!!! note "OAuth Handler Implementation"
    ```python
    from backend.integrations.oauth.base import BaseOAuthHandler

    class MyProviderOAuthHandler(BaseOAuthHandler):
        PROVIDER_NAME = "my_provider"

        def _get_authorization_url(self, scopes: list[str], state: str) -> str:
            # Implement URL generation for OAuth flow
            pass

        def _exchange_code_for_token(self, code: str, scopes: list[str]) -> dict:
            # Implement token exchange logic
            pass
    ```

### Webhook Support

Create a webhook manager in `_webhook.py`:

!!! note "Webhook Manager Implementation"
    ```python
    from backend.integrations.webhooks._base import BaseWebhooksManager

    class MyProviderWebhookManager(BaseWebhooksManager):
        PROVIDER_NAME = "my_provider"

        async def validate_event(self, event: dict) -> bool:
            # Implement event validation logic
            pass
    ```

## File Organization

```
backend/blocks/my_provider/
├── __init__.py          # Export your blocks
├── _config.py           # Provider configuration
├── _oauth.py           # OAuth handler (optional)
├── _webhook.py         # Webhook manager (optional)
├── _api.py             # API client wrapper (optional)
├── models.py           # Data models (optional)
└── my_block.py         # Block implementations
```

## Best Practices

1. **Error Handling**: Use `BlockInputError` for validation failures and `BlockExecutionError` for runtime errors (import from `backend.util.exceptions`). These inherit from `ValueError` so the executor treats them as user-fixable. See [Error Handling in new_blocks.md](new_blocks.md#error-handling) for details.
2. **Credentials**: Use the provider's `credentials_field()` method
3. **Validation**: Use SchemaField constraints (ge, le, min_length, etc.)
4. **Categories**: Choose appropriate categories for discoverability
5. **Advanced Fields**: Mark complex options as `advanced=True`
6. **Async Operations**: Use `async`/`await` for I/O operations
7. **API Clients**: Use `Requests()` from SDK or external libraries
8. **Testing**: Include test inputs/outputs for validation

## Common Patterns

### Making API Requests

```python
from backend.sdk import Requests

async def run(self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs):
    headers = {
        "Authorization": f"Bearer {credentials.api_key.get_secret_value()}",
        "Content-Type": "application/json"
    }

    response = await Requests().post(
        "https://api.example.com/endpoint",
        headers=headers,
        json={"query": input_data.query}
    )

    data = response.json()
    yield "results", data.get("results", [])
```

### Multiple Auth Types

```python
async def run(
    self,
    input_data: Input,
    *,
    credentials: OAuth2Credentials | APIKeyCredentials,
    **kwargs
):
    if isinstance(credentials, OAuth2Credentials):
        # Handle OAuth credentials
        token = credentials.access_token.get_secret_value()
    else:
        # Handle API key credentials
        token = credentials.api_key.get_secret_value()
```

!!! note "Authentication Types"
    - **`OAuth2Credentials`**: Access token via `credentials.access_token.get_secret_value()`
    - **`APIKeyCredentials`**: API key via `credentials.api_key.get_secret_value()`

### Handling Files

When your block works with files (images, videos, documents), use `store_media_file()`:

```python
from backend.data.execution import ExecutionContext
from backend.util.file import store_media_file
from backend.util.type import MediaFileType

async def run(
    self,
    input_data: Input,
    *,
    execution_context: ExecutionContext,
    **kwargs,
):
    # PROCESSING: Need local file path for tools like ffmpeg, MoviePy, PIL
    local_path = await store_media_file(
        file=input_data.video,
        execution_context=execution_context,
        return_format="for_local_processing",
    )

    # EXTERNAL API: Need base64 content for APIs like Replicate, OpenAI
    image_b64 = await store_media_file(
        file=input_data.image,
        execution_context=execution_context,
        return_format="for_external_api",
    )

    # OUTPUT: Return to user/next block (auto-adapts to context)
    result = await store_media_file(
        file=generated_url,
        execution_context=execution_context,
        return_format="for_block_output",
    )
    yield "image_url", result
```

!!! note "File Handling Patterns"
    - **PROCESSING**: Use `"for_local_processing"` when you need a local file path for tools like ffmpeg, MoviePy, PIL
    - **EXTERNAL API**: Use `"for_external_api"` when sending content to APIs like Replicate or OpenAI (returns base64 data URI)
    - **OUTPUT**: Use `"for_block_output"` to return results - this automatically adapts: `workspace://` in CoPilot, data URI in graphs

**Return format options:**
- `"for_local_processing"` - Local file path for processing tools
- `"for_external_api"` - Data URI for external APIs needing base64
- `"for_block_output"` - **Always use for outputs** - automatically picks best format

## Testing Your Block

```bash
# Run all block tests
poetry run pytest backend/blocks/test/test_block.py -xvs

# Test specific block
poetry run pytest 'backend/blocks/test/test_block.py::test_available_blocks[MyBlock]' -xvs
```

## Integration Checklist

- [ ] Add provider to `ProviderName` in `backend/integrations/providers.py` if using that enum in credentials; SDK integrations can register names through `ProviderBuilder`
- [ ] Define credentials with `_auth.py` helpers or the SDK provider configuration, and register the description and supported auth types in `_config.py`
- [ ] Implement block class with Input/Output schemas
- [ ] Generate unique block ID with `uuid.uuid4()`
- [ ] Choose appropriate block categories
- [ ] Implement `async run()` method
- [ ] Handle errors with `BlockInputError`/`BlockExecutionError` (don't wrap in try/except)
- [ ] Add test configuration (inline `test_input`/`test_output` and/or standalone `{name}_test.py`)
- [ ] Add integration icon PNG at `frontend/public/integrations/{provider_name}.png`
- [ ] Run `cd autogpt_platform/backend && poetry run python scripts/generate_block_docs.py` to generate docs
- [ ] Test the block
- [ ] Document any special requirements

## Example Blocks for Reference

- **Simple API**: `/backend/blocks/firecrawl/` - Basic API key authentication
- **OAuth + API**: `/backend/blocks/linear/` - OAuth and API key support
- **Webhooks**: `/backend/blocks/exa/` - Includes webhook manager

Study these examples to understand different patterns and approaches for building blocks.

---

### Platform Copilot Local Llm (`docs/platform/copilot-local-llm.md`)

# Running AutoPilot on a self-hosted LLM

> **Important**: This page covers the **AutoPilot chat path** — the
> conversational agent on `/copilot`. For the *block-layer* AI Text
> Generator block (used inside agent graphs you build yourself), see
> [Running Ollama with AutoGPT](ollama.md). The two paths read different
> env vars, so configuring one does not configure the other.
>
> Self-hosting only — the cloud `agpt.co` deployment routes AutoPilot
> through Anthropic / OpenRouter and ignores the variables below.

This guide makes the AutoPilot chat work **without an Anthropic, OpenAI,
or OpenRouter key** by routing it through any **OpenAI-compatible HTTP
endpoint you control**.

The transport is called `local` because it's the typical case, but
``CHAT_BASE_URL`` is just a URL — every deployment shape below works
equally well:

| Scenario | `CHAT_BASE_URL` |
| --- | --- |
| Ollama on the same Docker host (most common) | `http://192.168.1.42:11434/v1` (LAN IP) |
| Ollama on the same Docker host, via Docker Desktop | `http://host.docker.internal:11434/v1` |
| Ollama on a separate LAN box | `http://ollama.lab.local:11434/v1` |
| Ollama behind an HTTPS reverse proxy on the public internet | `https://ollama.example.com/v1` |
| [vLLM](https://docs.vllm.ai/en/latest/serving/openai_compatible_server.html), [LocalAI](https://localai.io/), [LM Studio](https://lmstudio.ai/), [LiteLLM proxy](https://docs.litellm.ai/docs/simple_proxy) | their respective `/v1` URLs |
| A managed OpenAI-compatible API you don't pay AutoGPT for | its `/v1` URL |

Anything that speaks the OpenAI `/v1/chat/completions` shape — including
`tools=[...]` for function calling — will work. The rest of this guide
uses Ollama as the running example because it's the easiest, but
substitute your own endpoint anywhere you see `http://...:11434/v1`.

## How it works

AutoPilot's `ChatConfig` (`backend/backend/copilot/config.py`) recognises
four chat transports. When `CHAT_USE_LOCAL=true`:

| Transport behaviour | Local |
| --- | --- |
| Routes the baseline (fast) path to `CHAT_BASE_URL` over OpenAI-compatible HTTP | ✅ |
| Supports the SDK / extended-thinking path (Claude Agent SDK) | ❌ — auto-downgrades to fast |
| `api_key` falls back to `OPEN_ROUTER_API_KEY` / `OPENAI_API_KEY` if `CHAT_API_KEY` is unset | ❌ — explicit `CHAT_API_KEY` only |
| Aux + advanced models (`title_model`, `simulation_model`, `fast_advanced_model`) inherit `fast_standard_model` if left at a cloud default | ✅ |
| Allows non-`anthropic/*` SDK model slugs (vendor validator skipped) | ✅ |

The downgrade is logged at WARNING when an `extended_thinking` request
arrives — there is no 500. The frontend toggle should already be hidden
because the `CHAT_MODE_OPTION` feature flag defaults off in
self-hosted deployments.

On the managed cloud platform (`BEHAVE_AS=cloud`), `CHAT_*_MODEL` env
vars are the *bottom* layer of model resolution: the `copilot-model-routing`
flag's per-user override → the LLM catalog's routing cell
(`backend/data/llm_registry/catalog.py`) → env default, with slugs
unknown to the catalog or disabled in it refused at serve time.

**On self-hosted installs — any transport — the catalog's routing cells
are skipped entirely**: they are the cloud deployment's config traveling
in the shipped file, and they never override your `CHAT_*_MODEL`
configuration. Resolution here is the `copilot-model-routing` flag →
`CHAT_*_MODEL`, exactly
the pre-catalog behavior; your env vars stay authoritative. See
[Managing LLM Models](contributing/managing-llm-models.md).

## Required environment variables

In `autogpt_platform/backend/.env` (for the single-container image, the same
lines go in its `--env-file`; see
[Run AutoGPT in One Docker Container](single-container.md#ollama-or-another-local-openai-compatible-server)):

```bash
# Turn on the local transport
CHAT_USE_LOCAL=true

# Where the OpenAI-compatible endpoint lives. From inside the docker
# containers this must NOT be 127.0.0.1 / localhost — use the host's LAN
# IP (e.g. 192.168.1.42) or, if you've added the directive to compose,
# host.docker.internal:host-gateway.
CHAT_BASE_URL=http://192.168.1.42:11434/v1

# Any non-empty string — Ollama doesn't validate it. The local transport
# deliberately does NOT fall back to OPENAI_API_KEY (which is usually
# present for graphiti / embedders), so this must be set explicitly.
CHAT_API_KEY=ollama

# The chat model. Bare model names ONLY — provider/model slugs (e.g.
# `anthropic/claude-...`) are passed through verbatim and Ollama can't
# resolve them. See "Picking a model" below.
CHAT_FAST_STANDARD_MODEL=hf.co/ornith-ai/Ornith-1.5-9B-GGUF:Q4_K_M

# Optional — override for the advanced tier. If you leave it out, the
# local transport derives title_model, simulation_model, AND
# CHAT_FAST_ADVANCED_MODEL from CHAT_FAST_STANDARD_MODEL automatically
# (see _apply_local_aux_models in backend/backend/copilot/config.py),
# so the advanced toggle never sends a cloud-only slug to Ollama. Set
# it explicitly only if you want a bigger model for the advanced tier
# and have the VRAM for it.
CHAT_FAST_ADVANCED_MODEL=qwen3:14b-q4_K_M
```

## Picking a model

The platform's chat loop calls **OpenAI-style tool-calling** on every
turn, streams responses, and ships an ~8 k-token system prompt. Pick a
model that handles all three.

| Tier | Recommended Ollama tag | Why | Footprint |
| --- | --- | --- | --- |
| **Default** | `hf.co/ornith-ai/Ornith-1.5-9B-GGUF:Q4_K_M` | Official Ornith GGUF; agentic 9B model with OpenAI-compatible tool calling; 262,144-token native context; reasoning model (the chat UI renders its thinking separately from the answer) | ~5.8 GB model file; allow additional RAM for context and the KV cache |
| **Tight RAM** | `qwen3:4b` | Smaller; native tools; set `think: false` to avoid the unclosed-`<think>` tool-call render bug | ~3-4 GB resident |
| **GPU / advanced** | `qwen3:14b-q4_K_M` | Best tool-selection accuracy in this size class | ~12 GB VRAM |

Pull whichever you choose:

```bash
ollama pull hf.co/ornith-ai/Ornith-1.5-9B-GGUF:Q4_K_M
```

## Context window — set it once, on the backend

Ollama defaults `num_ctx` to **4096 tokens regardless of the model's
advertised window** ([ollama/ollama#2714](https://github.com/ollama/ollama/issues/2714)).
That's smaller than AutoPilot's system prompt + tool schemas — without a
larger window Ollama only sees the *end* of the instructions and responses
are incoherent or 500 outright.

Set the window **once, on the server**, via `OLLAMA_CONTEXT_LENGTH`. There is
**no AutoGPT-side context config** to keep in sync: AutoPilot reads the
backend's *actual* loaded window back at runtime — Ollama `/api/ps`, llama.cpp
`/props`, vLLM / LM Studio `/v1/models` — and compacts the conversation under
it. Backends that don't report a window (LiteLLM proxy, Jan,
text-generation-webui) fall back to assuming 32k.

The default Ornith model has a 262,144-token native window, so the installer
sets `OLLAMA_CONTEXT_LENGTH=262144`. This maximizes available conversation
history but substantially increases KV-cache RAM/VRAM use. Operators using a
custom model or constrained hardware can lower it, but should keep at least
24k: below that, the system prompt + tools leave almost no room for
conversation and AutoPilot logs a warning.

The installer sets `OLLAMA_CONTEXT_LENGTH` for you. Manual setup per platform:

**Linux** (systemd drop-in):

```ini
# /etc/systemd/system/ollama.service.d/host.conf
[Service]
Environment="OLLAMA_HOST=0.0.0.0:11434"
Environment="OLLAMA_CONTEXT_LENGTH=262144"
```

Then `sudo systemctl daemon-reload && sudo systemctl restart ollama`.

**macOS** (launchctl, persists across logins for launchd-spawned processes):

```bash
launchctl setenv OLLAMA_HOST 0.0.0.0:11434
launchctl setenv OLLAMA_CONTEXT_LENGTH 262144
# Then restart Ollama — either:
brew services restart ollama        # if installed via the brew formula
# …or quit the menu-bar app and relaunch it (the .dmg install)
```

**Windows** (user-scope env vars, persists across reboots):

```powershell
setx OLLAMA_HOST "0.0.0.0:11434"
setx OLLAMA_CONTEXT_LENGTH "262144"
# Then quit Ollama from the system tray and relaunch it
# (setx writes to HKCU but does NOT update already-running processes).
```

Verify on any platform with `ollama ps` (the `CONTEXT` column should
show your value, e.g. 262144). If you change it, AutoPilot picks up the
new window automatically on the next turn — nothing else to update.

## Networking — same host, different host, or remote

The endpoint can be on the same machine, on the LAN, or anywhere
internet-reachable. Pick whichever matches your deployment shape:

### Same host as the AutoGPT containers

How containers reach the host depends on whether you're on Docker
Desktop (macOS / Windows) or native Docker (Linux):

**macOS + Windows (Docker Desktop)** — every container already has a
`host.docker.internal` entry pointing at the host. No extra wiring:

```bash
CHAT_BASE_URL=http://host.docker.internal:11434/v1
```

Still set `OLLAMA_HOST=0.0.0.0:11434` so the .app/tray-managed Ollama
accepts the connection from the Desktop network — by default it binds
only to `127.0.0.1`.

**Linux (native Docker)** — there's no auto-injected
`host.docker.internal`. Pick one:

1. **Use the LAN IP** in `CHAT_BASE_URL` — simplest, works everywhere.
2. **Bind Ollama to all interfaces:** `OLLAMA_HOST=0.0.0.0:11434` (set
   in the systemd unit or a drop-in), so containers reach it via the
   bridge gateway.
3. **Add `extra_hosts: ["host.docker.internal:host-gateway"]`** to the
   chat services in `autogpt_platform/docker-compose.yml`.

The bundled installer does these for you on a fresh box:

| Platform | Command |
| --- | --- |
| Linux | `installer/setup-autogpt.sh --with-ollama` |
| macOS | `installer/setup-autogpt.sh --with-ollama` |
| Windows | `installer\setup-autogpt.bat /with-ollama` |

### Different LAN box (dedicated GPU server, NAS, …)

Set `CHAT_BASE_URL` to the box's hostname or IP:

```bash
CHAT_BASE_URL=http://gpu-rig.lab.local:11434/v1
```

On the Ollama box, set `OLLAMA_HOST=0.0.0.0:11434` so it accepts
non-loopback connections, and either open port 11434 in the firewall
for your AutoGPT host's IP or put both behind a private VPN /
WireGuard mesh.

### Remote / public-internet endpoint

Two approaches, in increasing order of "please do this":

1. **Trusted private network** (Tailscale, WireGuard, ZeroTier,
   corporate VPN). Treat the remote endpoint exactly like a LAN box.
2. **Public HTTPS with auth** — terminate TLS at a reverse proxy
   (Caddy, nginx, Cloudflare Tunnel) in front of Ollama / vLLM /
   whatever, and require a bearer token. Set:

   ```bash
   CHAT_BASE_URL=https://ollama.example.com/v1
   CHAT_API_KEY=<the-bearer-the-proxy-checks>
   ```

> **Do not** expose raw Ollama on the public internet. Ollama itself
> performs **no authentication** — anyone who can reach `:11434` can
> use (and exhaust) your model. Always front it with a proxy that
> enforces a token.

## Other platform features that use the local transport

The `local` transport isn't just for AutoPilot chat. The same client
flows to every backend helper that needs an LLM, so a single
`CHAT_USE_LOCAL=true` install also covers:

- **Dry-run block simulator** — when a user clicks "Test" in the agent
  builder, blocks role-play their execution against an LLM rather than
  hitting external APIs. Uses `ChatConfig.simulation_model`
  (auto-derived to `fast_standard_model` under local).
- **Onboarding business-understanding extraction** — the post-signup
  Tally form is extracted into structured suggestions via the LLM.
  Uses `ChatConfig.title_model`.
- **Long-run prompt compression** — the chat / agent loop summarizes
  message history when context grows beyond a threshold.
- **Marketplace semantic search** — the store generates embeddings for
  agent descriptions to power hybrid (lexical + semantic) ranking.
  Hybrid search degrades gracefully to lexical-only when no embedding
  backend is available.

### Embeddings (marketplace search, agent uploads)

The store's embedding model is overridable via env so deployments with
a compatible backend (vLLM, LiteLLM proxy, Ollama with an embedding
model pulled, Azure OpenAI) can swap models without a code change.
**The replacement model must emit 1536-dim vectors** — the pgvector
column is declared `vector(1536)` in `schema.prisma` and inserts with
any other dim hard-fail.

```bash
# Default — OpenAI text-embedding-3-small (1536 dim):
STORE_EMBEDDING_MODEL=text-embedding-3-small

# Example: nomic-embed-text on Ollama emits 768 dims natively, so it
# DOES NOT fit the existing schema — picking it would break every
# publish + reindex. Use a 1536-dim model instead, e.g.
# text-embedding-ada-002 (OpenAI legacy) or one of the LiteLLM proxy's
# 1536-dim shims. Custom-dim support would need a schema migration
# beyond the scope of this guide.
```

> **pgvector dimension is fixed in the schema, not configurable at
> runtime.** A model that emits a different vector length will succeed
> at the embedding call and fail at every subsequent `INSERT`. If you
> need a different dim, you'll need to fork the schema and migrate
> existing rows — it's not a runtime knob.

If you don't configure an embedding backend at all, marketplace
hybrid search auto-degrades to lexical-only (no semantic ranking) —
not fatal, just less smart.

## Verifying the wiring

After `docker compose up -d`:

```bash
# 1. CHAT_USE_LOCAL is in the live container env
docker exec autogpt_platform-copilot_executor-1 env | grep ^CHAT_
#   CHAT_USE_LOCAL=true
#   CHAT_BASE_URL=http://192.168.1.42:11434/v1
#   ...

# 2. Send a turn from the UI, then confirm baseline routing in the log
docker logs autogpt_platform-copilot_executor-1 | grep -E "Using.*service"
#   [CoPilotExecutor|...] Using baseline service (mode=default)

# 3. Confirm Ollama saw the request — per platform:

# Linux (systemd-managed Ollama):
journalctl -u ollama --since "1 minute ago" | grep "POST"
#   [GIN] ... | 200 | 7.5s |  ... | POST "/v1/chat/completions"

# macOS (brew formula):
tail -F "$(brew --prefix)/var/log/ollama.log" | grep "POST"
# macOS (.app from ollama.com): logs live in ~/.ollama/logs/server.log
tail -F ~/.ollama/logs/server.log | grep "POST"

# Windows: the Ollama tray app writes to %LOCALAPPDATA%\Ollama\server.log
powershell -Command "Get-Content $env:LOCALAPPDATA\Ollama\server.log -Wait | Select-String POST"
```

If `Using baseline service` appears and Ollama logs a 200, the
end-to-end path is working — any remaining errors are model / RAM /
quantization concerns rather than config-routing bugs.

## Troubleshooting

**Frontend shows "The assistant encountered an error"** — check the
copilot_executor log for the upstream error. Common causes:
- `model requires more system memory (X GiB) than is available (Y GiB)`
  → free RAM (stop ClamAV, raise VM memory) or pick a smaller model
- `model "..." not found` → `ollama pull <slug>` first
- `connection refused` → containers can't reach the host on `:11434`;
  see "Networking — same host, different host, or remote" above

**`api_key` is `None` even though I set `OPENAI_API_KEY`** — by design.
The local transport requires an explicit `CHAT_API_KEY` so a stray cloud
key set for graphiti / embedders doesn't silently bind to your local
backend as the bearer token.

**Title generation fails / returns "Untitled chat"** — `title_model`
should auto-inherit `fast_standard_model` under the local transport. If
you've explicitly set `CHAT_TITLE_MODEL=openai/gpt-4o-mini` somewhere,
remove it.

**Slow first response** — Ollama loads the model into RAM on the first
request, which can take 5-15 s for 8 B models on CPU. Subsequent
requests are much faster while the model stays resident.

**Every AutoPilot turn takes minutes on CPU** — expected on CPU-only
hosts, not a hang. AutoPilot ships an ~8 k-token system prompt and the
model must *prefill* (compute KV-cache state for) every token of that
prompt before the first output token is emitted. On 4 CPU cores an
8 B Q4 model prefills at roughly **3-4 tokens/sec**, so a fresh turn
takes ~35-45 min just to start generating. Title generation (~70-token
prompt) finishes in seconds because there's almost nothing to prefill.
A consumer GPU brings this down to seconds. If you're CPU-only and
just want to validate the install end-to-end, tail the Ollama server
log (see the per-platform commands in "Verifying the wiring" above)
and watch for the `POST /v1/chat/completions` line — once it appears
with a 200, prefill finished and the model is generating.

## Dream pass + memory under local transport

The graphiti memory layer and the nightly dream pass both ride the
same self-hosted backend `CHAT_USE_LOCAL=true` points at. Three
things you should know:

### Dream pass runs sync-baseline only

The dream pass's batch path (Anthropic batch, OpenAI batch) is
provider-locked and unavailable on local backends. `CHAT_USE_LOCAL=true`
forces `execution_path="sync_baseline"` regardless of which API keys
might be set elsewhere on the box — your local LLM handles all three
phases (consolidate / recombine / sanitize) on the same endpoint as
chat. Cost-log rows label `provider="ollama"` so the admin
platform-costs dashboard distinguishes them from cloud spend.

### Memory uses the chat models by default

When `CHAT_USE_LOCAL=true`, `GraphitiConfig._apply_local_graphiti_models`
rewrites the cloud OpenAI defaults to local Ollama equivalents:

| Setting | Cloud default | Local default |
|---|---|---|
| `GRAPHITI_LLM_MODEL` | `gpt-6-luna` | `hf.co/ornith-ai/Ornith-1.5-9B-GGUF:Q4_K_M` |
| `GRAPHITI_RERANKER_MODEL` | `gpt-6-luna` | `hf.co/ornith-ai/Ornith-1.5-9B-GGUF:Q4_K_M` |
| `GRAPHITI_EMBEDDER_MODEL` | `text-embedding-3-small` | `nomic-embed-text` |

The LLM + reranker reuse the same Ornith 1.5 9B model the `--with-ollama`
installer already pulls for chat, so no extra `ollama pull` is needed
unless you've overridden them. The embedder is a separate model — see
the next section.

You can pin your own slugs at any time by setting the matching
`GRAPHITI_*_MODEL` env var; the validator only touches slots still at
their cloud default, including the previous `gpt-4.1-mini` default in
existing `.env` files. A custom slug (`qwen3:8b`, `hf.co/...`,
`my-registry.io/model:tag`) passes through untouched.

### Embeddings require an embedding model pulled into Ollama

Ollama doesn't ship an embedding model in its default model set, so
graphiti's per-turn entity extraction will 404 on `/v1/embeddings`
until you pull one:

```bash
ollama pull nomic-embed-text
```

Without it, chat still works, but `graphiti.add_episode(...)` fails
silently per turn — the agent loses memory of the conversation
between sessions. With it pulled, graphiti round-trips
embeddings against the same Ollama endpoint as the LLM, and
warm-context retrieval works end-to-end on the local stack.

If you'd rather use a different embedding model (e.g.
`mxbai-embed-large` for higher recall at higher disk cost), pull
that and set `GRAPHITI_EMBEDDER_MODEL=<slug>` to override the
local default.

### Community rebuild stays on sync tier

`graphiti_config.community_rebuild_use_flex_tier=True` (the default)
is treated as a *request*, not a guarantee. OpenAI's flex tier only
delivers the ~50% discount through OpenRouter's pass-through to
OpenAI / Google upstreams, so on local + Anthropic transports the
flex client is silently swapped for the regular `OpenAIClient`
(logged at INFO). The weekly community rebuild still runs — at full
sync price, which on local Ollama is `$0`.

### Subscription mode caveat

If you also use Claude Code subscription (`CHAT_USE_CLAUDE_CODE_SUBSCRIPTION=true`)
for the chat path, the dream pass needs a separate `ANTHROPIC_API_KEY`
set in the environment. The Claude Code OAuth token authenticates the
chat CLI only; per Anthropic's Feb-2026 ToS update, OAuth tokens
**cannot** call the Messages API directly. Without `ANTHROPIC_API_KEY`,
the dream pass writes an `errored` JobStatus with a friendly hint
pointing you at this section.

A separate Anthropic Agent-SDK credit pool launches **2026-06-15**
($20 Pro / $100 Max-5x / $200 Max-20x at standard API rates,
one-time opt-in). Once that lands, subscription users will have
the option of routing the dream pass through the Agent SDK instead
of the Messages API — coverage tracked as a post-Jun-15 follow-up.

---

### Platform Create Basic Agent (`docs/platform/create-basic-agent.md`)

# **Creating a Basic AI Agent with AutoGPT**

## **Overview**

This guide walks through creating a simple question-answer AI agent using AutoGPT's visual builder. This is a basic example that can be expanded into more complex agents.

## **Prerequisites**

### **Cloud-Hosted AutoGPT**
If you're using the cloud-hosted version at [agpt.co](https://agpt.co), you're ready to go! AI blocks come with **built-in credits** — no API keys required to get started. If you'd prefer to use your own API keys, you can add them via **Profile → Integrations**.

### **Self-Hosted (Docker)**
If you're running AutoGPT yourself, you'll need to add your own API keys. Where they go depends on how you installed it.

**Docker Hub image** (`significantgravitas/autogpt`): the backend reads these keys from the container's environment. Put them in an environment file, for example `autogpt.env` (or pass each one with `-e`):

```bash
# autogpt.env
OPENAI_API_KEY=sk-your-key-here
ANTHROPIC_API_KEY=sk-ant-your-key-here
# Add other provider keys as needed
```

Then replace the container so it starts with those keys, keeping the same `autogpt-data` volume:
```bash
docker stop autogpt && docker rm autogpt
# then rerun your original docker run command with --env-file autogpt.env added
```

**Appliance installer**: add the keys to the installer's private `autogpt.env`, then recreate the container as described in [Runtime Configuration](installer.md#runtime-configuration).

**Docker Compose from source**: add the same keys to `autogpt_platform/backend/.env`, then restart the services from `autogpt_platform/`:
```bash
docker compose down && docker compose up -d
```

AutoPilot has its own provider settings; see [Models and memory](single-container.md#models-and-memory).

**Note:** The Calculator example below doesn't require any API credentials — it's a good way to test your setup before adding AI blocks.

## **Example Agent: Q&A (with AI)**

A step-by-step guide to creating a simple Q&A agent using input and output blocks.

<center><iframe width="560" height="315" src="https://www.youtube.com/embed/ih57vRbH0H0?si=PGHx_qquYpXofiu_" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></center> 

## **Components Required**
1. Input Block
2. AI Text Generator Block
3. Output Block

## **Step-by-Step Instructions**

### **1. Set Up the Basic Structure**
1. Add an Input Block
2. Add an AI Text Generator Block
3. Add an Output Block

### **2. Connect the Components**
1. Connect the Input Block to the AI Text Generator's Prompt
2. Connect the AI Text Generator's response to the Output Block's value

### **3. Configure Block Names**
* Name the Input Block: "question"
* Name the Output Block: "answer"

### **4. Save the Agent**
1. Click the Save button
2. Name your agent (e.g., "question and answer")

### **5. Test the Agent**
1. Click the Run button
2. Enter a question in the input field (e.g., "How far is Earth from Pluto?")
3. View the results through either:
    * The "View More" option
    * The "Agent Outputs" section

## **Viewing Results**

You can access the AI's response in two ways:

* Click "View More" for detailed results
* Check the "Agent Outputs" section for the response

## **Example Agent: Calculator (without AI)**

A step-by-step guide to creating a simple calculator agent using input and output blocks.

<center><iframe width="560" height="315" src="https://www.youtube.com/embed/ESLKHcXxRvA?si=i2L2sloLskSMO8_I" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></center>

##**Components Required**
* Input Block (2 instances)
* Calculator Block
* Output Block

## **Setup Instructions**

### **1. Adding Input Blocks**
1. Add two input blocks, positioned one below the other
2. Name the first input block "a"
3. Name the second input block "b"

### **2. Adding Calculator Block**
1. Add a calculator block to your workspace
2. Position it after the input blocks

### **3. Adding Output Block**
1. Add an output block
2. Name it "results"

### **4. Connecting the Blocks**
1. Connect the "a" input block to the "a" input on the calculator block
2. Connect the "b" input block to the "b" input on the calculator block
3. Connect the calculator block's result to the value input of the output block

### **5. Saving Your Agent**
1. Name your agent (e.g., "Simple Calculate Agent")
2. Save the configuration

## **Testing Your Calculator**

### **Running Calculations**
1. Enter values in both input blocks
2. Select the desired operation (e.g., multiply)
3. Click the "Run" button to execute the calculation

### **Viewing Results**

There are two ways to view the calculation results:

1. Click "View More" to see the detailed output
2. Check the "Agent Outputs" section, which displays results from the output block

## **Example Calculation**
* Input A: 227
* Input B: 17
* Operation: Multiply
* Expected output will be shown in the results section

## **Tips**
* Always save your agent after making changes
* Verify all connections are properly made before running
* Use the "View More" option for detailed output information

## **Note**
While these are basic example, you can enhance the agent with additional blocks and functionality to create more complex interactions.

---

### Platform Credits And Billing (`docs/platform/credits-and-billing.md`)

# Credits & Billing

## Overview

The AutoGPT Platform uses automation credits for metered agent execution. This guide explains how usage is priced, how to review costs, and how to manage your balance.

{% hint style="info" %}
Plans and pricing can change. See the [pricing page](https://agpt.co/pricing) for current details.
{% endhint %}

## How Credits Work

Credits are charged when blocks run. Depending on the block, the final charge may be fixed or based on actual usage, such as tokens, provider cost, elapsed time, data size, or number of items. For usage-based blocks, the platform may estimate a charge before execution and reconcile it against actual usage afterward.

### Block Pricing

Block pricing varies by block, configuration, and usage. Review the estimate shown in the platform before running an agent; the final charge for usage-based blocks may differ from the estimate.

Your total cost depends on your plan and automation-credit usage. See the [pricing page](https://agpt.co/pricing) for current subscription and included-credit details.

## Checking Your Balance

Your balance is available from the automation-credits wallet in the platform navigation and from [Billing settings](https://platform.agpt.co/settings/billing?tab=automation-credits).

## Viewing Task Costs

To review the cost of a specific agent run:

1. Go to your [Agent Library](agent-library.md)
2. Open the agent
3. Click on a completed task in the left-hand pane
4. The task detail header displays **Cost** in dollars.

{% hint style="info" %}
Use the **Automation Credits** section in [Billing settings](https://platform.agpt.co/settings/billing?tab=automation-credits) for account-level usage and transaction history. Individual task costs are also available within each agent.
{% endhint %}

## Running Out of Credits

If your automation-credit balance is exhausted, paid blocks cannot start until credits are added. Scheduled tasks may fail for insufficient balance. Final reconciliation for a usage-based block that already ran can leave the balance below zero.

You can add credits manually or configure auto-refill from Billing settings.

## Adding Credits

1. Open **Settings** from your profile menu.
2. Select **Billing**.
3. Open the **Automation Credits** tab.
4. Select **Add credits** and follow the checkout prompts.

You can also go directly to [Billing settings](https://platform.agpt.co/settings/billing?tab=automation-credits).

---

### Platform D Id (`docs/platform/d_id.md`)

# Find available voices for D-ID

1. **ElevenLabs**
   - Select any voice from the voice list: https://api.elevenlabs.io/v1/voices
   - Copy the voice_id
   - Use it as a string in the voice_id field in the CreateTalkingAvatarClip Block

2. **Microsoft Azure Voices**
    - Select any voice from the voice gallery: https://speech.microsoft.com/portal/voicegallery
    - Click on the "Sample code" tab on the right
    - Copy the voice name, for example: config.SpeechSynthesisVoiceName ="en-GB-AbbiNeural"
    - Use this string en-GB-AbbiNeural in the voice_id field in the CreateTalkingAvatarClip Block

3. **Amazon Polly Voices**
    - Select any voice from the voice list: https://docs.aws.amazon.com/polly/latest/dg/available-voices.html
    - Copy the voice name / ID
    - Use it as string in the voice_id field in the CreateTalkingAvatarClip Block

---

### Platform Data Flow And Execution (`docs/platform/data-flow-and-execution.md`)

# Data Flow & Execution

## Overview

Understanding how agents execute is key to building effective workflows. This guide explains how data flows through an agent, what determines execution order, and how to work with lists and errors.

## Execution Order

Agent execution follows the graph's starting nodes, data connections, configured values, and input validation.

### How It Works

1. Execution starts from every starting node: an Agent Input block or any executable block with no inbound links.
2. A downstream block is queued when its required inputs validate after upstream outputs and configured values are applied.
3. Execution continues until no more downstream blocks can be queued; conditional branches and skipped nodes mean not every block necessarily runs.
4. Output blocks collect any results that reach them.

### Required Inputs

A block will only execute when:

- All **connected input pins** have received data from their upstream blocks
- All **required input pins** have values — either from a connection or from a hardcoded value set directly on the block

This means you can have blocks that don't depend on each other execute in any order, while blocks that depend on the output of another block will always wait.

## Working with Pins

### Pin Types

Input and output pins are typed. Common types include:

- **Text**: String values
- **Number**: Numeric values
- **File**: File uploads or downloads
- **List**: Arrays of items
- **Boolean**: True/false values
- **Object**: Structured data

Connections can only be made between compatible pin types.

### Data Flow Visualisation

When an agent is running, you can see data moving through the workflow in real time. Data flow is represented by a **coloured bead** that slides along each connection line from the output pin to the input pin, giving you a clear visual of what's happening.

## Working with Lists

Blocks can handle list data in flexible ways:

- **Outputting lists**: Some blocks produce a list of items as their output. You can choose to receive the full list as a single output or receive individual items one at a time.
- **Iterating over lists**: You can send a list into a block that iterates through its contents, yielding each item one by one. This is useful for processing each item in a list independently.

This makes it straightforward to build agents that process batches of data — for example, fetching a list of URLs and then processing each one through an AI block.

## Error Handling

When a block fails during execution, it does **not** automatically stop the entire agent. Instead:

1. The failed block produces data on its **error pin**
2. What happens next depends on how you've wired the agent

### Handling Errors Gracefully

You have full control over error handling through the block connections:

- **Surface the error**: Connect the error pin to an output block to return the error as part of the agent's result. This is useful for debugging or when you want users to see what went wrong.
- **Handle and continue**: Connect the error pin to other blocks that provide fallback behaviour. For example, retry with different settings, use a default value, or route to an alternative workflow path.
- **Ignore the error**: If the error pin is not connected, the error data is simply not propagated. Downstream blocks that depend on the failed block's normal output pins will not execute (since their inputs won't be satisfied).

{% hint style="info" %}
Building robust agents means thinking about what happens when things go wrong. Consider connecting error pins to output blocks during development so you can see any issues, then add proper error handling once your agent is working.
{% endhint %}

## Execution Summary

| Concept | How It Works |
|---------|-------------|
| **Execution order** | Determined by starting nodes, data connections, configured values, and input validation |
| **Starting point** | Agent Input blocks and executable blocks with no inbound links |
| **Ending point** | Output blocks collect final results |
| **Parallel execution** | Blocks with no dependencies on each other can execute in any order |
| **Error handling** | Failed blocks yield data on their error pin — you decide what to do with it |
| **Lists** | Can be processed as a whole or iterated item by item |
| **Visual feedback** | Coloured beads slide along connection lines during execution |

---

### Platform Delete Agent (`docs/platform/delete-agent.md`)

# **How to Delete an Agent in AutoGPT**

## **Overview**

Is your AutoGPT Monitor getting cluttered or did you create an agent you no longer see? Learn how to quickly delete unwanted agents. 

<center><iframe width="560" height="315" src="https://www.youtube.com/embed/oMZklozNga4?si=b2ubsGd5AST0gN-p" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></center>

## **Steps to Delete an Agent**
1. **Locate the Agent**
    * Navigate to the Monitor Tab in the AutoGPT builder
    * Find the agent you want to delete from the list
2. **Select the Agent**
    * Click on the agent you wish to delete
3. **Delete the Agent**
    * Look for the trash icon on the right side of the interface
    * Click the trash icon
    * A confirmation dialog will appear asking "Are you sure you want to delete this agent?"
    * Click "Yes, delete" to confirm

Once confirmed, the agent will be immediately removed from your list.

**Note**: This action cannot be undone, so make sure you want to permanently delete the agent before confirming.

---

### Platform Download Agent From Marketplace Local (`docs/platform/download-agent-from-marketplace-local.md`)

# **How to Download and Import an Agent from the AutoGPT Marketplace (Local Hosting)**

## **Overview**
This guide explains how to download an agent from the AutoGPT marketplace and import it into your locally hosted instance.

<center><iframe width="560" height="315" src="https://www.youtube.com/embed/eTg2kbJdBIw?si=v1npcr8HDiInvUPw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></center>

## **Prerequisites**
* A running self-hosted AutoGPT instance
* Access to the public marketplace at [platform.agpt.co/marketplace](https://platform.agpt.co/marketplace)

## **Step-by-Step Process**

### **1. Download the Agent**
1. Open the public marketplace and click the agent you want
2. Under "Want to use this agent locally?", click **Download here**
3. The agent file (JSON) saves to your computer

### **2. Import the Agent**
1. In your self-hosted instance, open **Agents**
2. Click **Import**
3. On the **AutoGPT agent** tab, select the downloaded agent file
4. Check the agent name and description
5. Click **Upload**

### **3. Verify Import**
* The agent opens in the Builder
* It also appears in your **Agents** library

##  **Important Notes**
* Before running it, connect the credentials its blocks need under **Settings → Integrations**
* Deployment-level provider keys and OAuth app credentials are set as environment variables instead; see [Self-Hosted Credentials](integrations-and-credentials.md#self-hosted-credentials)

---

### Platform Edit Agent (`docs/platform/edit-agent.md`)

# **How to Edit an Agent in AutoGPT**

## **Overview**

This guide explains how to access and edit existing agents in AutoGPT, whether they're your own creations or downloaded from the marketplace.

<center><iframe width="560" height="315" src="https://www.youtube.com/embed/-AKPW5N1O8k?si=iSEFrUAcS7R8ctfL" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></center>

## **Steps to Edit an Agent**

### **1. Access the Monitor Tab**
* Navigate to the Monitor Tab in the AutoGPT builder
* You'll see a list of all your agents, including:
    * Agents you've created
    * Agents downloaded from the marketplace

### **2. Select the Agent**
* Locate the agent you want to edit from the list
* Click on the agent name to select it

### **3. Enter Edit Mode**
* Look for the pencil icon next to the selected agent
* Click the pencil icon to open the editor


### **4. Modify the Agent**
* The agent will load in the editor with all its existing components
* Make your desired modifications to the agent's configuration

### **5. Save Changes**
* After making your modifications, save the updated agent

## **Note**
* All agents can be edited, regardless of whether they were created by you or downloaded from the marketplace
* Changes are saved to your local instance

---

### Platform Gemini (`docs/platform/gemini.md`)

# Using Google Gemini with AutoGPT

This guide covers integrating Google Gemini models with AutoGPT using OpenRouter.

---

## Prerequisites

1. Make sure you have completed the [AutoGPT Setup Guide](https://agpt.co/docs/platform/getting-started) and have AutoGPT running locally at `http://localhost:3000`.
2. You have an **OpenRouter API key** from [OpenRouter](https://openrouter.ai/keys).

---

## Getting Your API Key

AutoGPT routes all Gemini models through OpenRouter. You need an OpenRouter API key:

1. Visit [OpenRouter Keys](https://openrouter.ai/keys)
2. Sign in or create an account
3. Click **"Create Key"**
4. Copy the generated key for use in AutoGPT

---

## Setup Steps

### 1. Start AutoGPT Locally

Follow the official guide:
[AutoGPT Getting Started Guide](https://agpt.co/docs/platform/getting-started)

Ensure AutoGPT is running and accessible at:
[http://localhost:3000](http://localhost:3000)

### 2. Open the Visual Builder

Open your browser and navigate to:
[http://localhost:3000/build](http://localhost:3000/build)

Or click **"Build"** in the navigation bar.

### 3. Add an AI Text Generator Block

1. Click the **"Blocks"** button on the left sidebar.
2. In the search bar, type `AI Text Generator`.
3. Drag the block into the canvas.

### 4. Select a Gemini Model

Click the AI Text Generator block to configure it.

In the **LLM Model** dropdown, select one of the available Gemini models:

| Model | Model ID |
| --- | --- |
| Gemini 3.1 Pro Preview | `google/gemini-3.1-pro-preview` |
| Gemini 3.1 Flash Lite Preview | `google/gemini-3.1-flash-lite-preview` |
| Gemini 3 Flash Preview | `google/gemini-3-flash-preview` |
| Gemini 2.5 Pro | `google/gemini-2.5-pro` |
| Gemini 2.5 Flash | `google/gemini-2.5-flash` |
| Gemini 2.5 Flash Lite | `google/gemini-2.5-flash-lite` |
| Gemini 2.0 Flash 001 | `google/gemini-2.0-flash-001` |
| Gemini 2.0 Flash Lite 001 | `google/gemini-2.0-flash-lite-001` |

> Select the models prefixed with `google/` in the dropdown.

### 5. Configure Your Credentials

Inside the **AI Text Generator** block:

1. **API Key**: Enter your OpenRouter API key
2. **Prompt**: Enter your desired prompt text

Get your API key from:
[https://openrouter.ai/keys](https://openrouter.ai/keys)

> Save your API key as a credential in AutoGPT for easy reuse across multiple blocks.

### 6. Save Your Agent

Click the **"Save"** button at the top-right of the builder interface:

1. Give your agent a descriptive name (e.g., `gemini_research_agent`)
2. Click **"Save Agent"** to confirm

### 7. Run Your Agent

From the workspace:

1. Click **"Run"** next to your saved agent
2. The request will be sent to the selected Gemini model

### 8. View the Output

1. Scroll to the **AI Text Generator** block
2. Check the **Output** panel below it
3. Copy, export, or pass the result to additional blocks

---

## Gemini-Specific Features

### Multimodal Capabilities

Gemini models support multiple input types:

- **Text**: Standard text prompts and completions
- **Images**: Upload and analyze images
- **Code**: Programming and technical reasoning
- **Long Context**: Large context windows for document analysis

---

## Expand Your Agent

Enhance your workflow with additional blocks:

* **Tools** – Fetch URLs, call APIs, scrape data
* **Memory** – Retain context across interactions
* **Document Processing** – Analyze PDFs, text files
* **Web Search** – Combine with real-time information
* **Chains** – Create multi-step reasoning pipelines

---

## Pricing

Gemini models are priced through OpenRouter. Check current rates at:
[OpenRouter Google Models](https://openrouter.ai/google)

Pricing varies by model tier and usage volume.

---

## Troubleshooting

### API Key Issues
- Ensure you're using an **OpenRouter API key**, not a Google AI Studio key
- Verify the key has sufficient credits
- Check that the key is entered correctly without extra spaces

### Model Not Available
- Gemini models are accessed through OpenRouter
- Ensure you've selected a model with the `google/` prefix in the dropdown

### Rate Limiting
- Free tier has request limits per minute
- Upgrade to paid tier for production usage
- Consider using `google/gemini-2.5-flash-lite` for lower-cost, high-volume tasks

### Context Length Errors
- Each Gemini model has a maximum context window
- Split large tasks across multiple blocks for very long documents

---

## Additional Resources

- [Google AI Studio Documentation](https://ai.google.dev/gemini-api/docs)
- [Gemini API Quickstart](https://ai.google.dev/gemini-api/docs/quickstart)
- [Model Capabilities](https://ai.google.dev/gemini-api/docs/models)
- [OpenRouter Documentation](https://openrouter.ai/docs)
- [AutoGPT Platform Docs](https://agpt.co/docs/platform)

---

You are now set up to use Google Gemini models in AutoGPT.

---

### Platform Getting Started Cloud (`docs/platform/getting-started-cloud.md`)

# Getting Started with AutoGPT (Cloud)

## Introduction

This guide will get you up and running on the hosted AutoGPT Platform at [platform.agpt.co](https://platform.agpt.co). No local installation or Docker setup is required — just sign up and start building.

{% hint style="info" %}
Looking to self-host instead? Run the [single-container Docker image](single-container.md) or see the [Self-Hosting Guide](getting-started.md).
{% endhint %}

## Creating Your Account

1. Navigate to [platform.agpt.co](https://platform.agpt.co)
2. Click **Sign Up** and create your account
3. Once signed in, you'll land on the **AutoPilot** home screen

That's it — you're ready to go. Available credits and managed AI providers depend on your plan and account configuration. If a block requires credentials that are not included, the platform will prompt you to connect your own. See the [pricing page](https://agpt.co/pricing) for current plan details.

## Platform Navigation

The AutoGPT Platform has four main areas, accessible from the main navigation:

| Nav Button | URL | Description |
|------------|-----|-------------|
| **Home** | [platform.agpt.co](https://platform.agpt.co) | AutoPilot — your AI assistant for the platform |
| **Build** | [platform.agpt.co/build](https://platform.agpt.co/build) | Agent Builder — visual editor for creating agents |
| **Agents** | [platform.agpt.co/library](https://platform.agpt.co/library) | Your Agent Library — all your saved agents |
| **Marketplace** | [platform.agpt.co/marketplace](https://platform.agpt.co/marketplace) | Community marketplace for discovering agents |

Use the automation-credits wallet or **Billing** settings to review your balance. Account settings and integrations are available from **Settings**.

## What to Do First

Here are three great ways to get started:

### Option 1: Chat with AutoPilot

Click **Home** in the nav bar to open AutoPilot. You can ask it to perform supported actions such as browsing the marketplace, running agents, building agents, generating images, and conducting research. It's the fastest way to experience the platform.

### Option 2: Add an Agent from the Marketplace

1. Click **Marketplace** in the nav bar
2. Browse or search for an agent that interests you
3. Click on an agent to view its details
4. Click **Add to Library**
5. Navigate to **Agents** to find it in your library
6. Click on the agent and press **New agent task** to run it

### Option 3: Build Your Own Agent

1. Click **Build** in the nav bar to open the Agent Builder
2. Open the Blocks menu on the left-hand side
3. Add an **Input Block**, an **AI Text Generator Block**, and an **Output Block**
4. Connect them together by dragging between their pins
5. Press **Ctrl+S** (Windows/Linux) or **Cmd+S** (macOS), or click the save button to save your agent
6. Navigate to **Agents** to find and run it

For a detailed walkthrough, see [Creating a Basic Agent](create-basic-agent.md).

## Key Concepts

Before diving deeper, here are the core concepts you'll encounter:

- **Agent**: An automated workflow you design to perform specific tasks. Agents are made up of connected blocks.
- **Block**: A single action within an agent — such as generating text with AI, sending an email, or looking up data. The available block catalog depends on the platform deployment.
- **Task**: A single execution of an agent. When you run an agent with a set of inputs, that creates a task. You can view the task's inputs, outputs, and reported cost.
- **AutoPilot**: Your AI assistant that can perform supported platform actions through natural conversation.
- **Marketplace**: A public library of community-built agents you can add to your own library.
- **Credits**: The currency used for metered block execution. Pricing varies by block and usage; review your balance from the automation-credits wallet or Billing settings.

## Next Steps

| Guide | Description |
|-------|-------------|
| [AutoPilot](autopilot.md) | Learn what AutoPilot can do for you |
| [Agent Builder Guide](agent-builder-guide.md) | Master the visual agent builder |
| [Agent Library](agent-library.md) | Manage your agents, tasks, and schedules |
| [Marketplace](marketplace.md) | Discover and share agents |
| [Credits & Billing](credits-and-billing.md) | Understand the credit system |

---

### Platform Installer (`docs/platform/installer.md`)

# AutoGPT Platform Release Installer

The release installer runs the published AutoGPT Platform appliance as one
Docker container. It does not clone the repository, build AutoGPT, install
Docker, elevate privileges, or accept Docker Desktop license terms.

The appliance exposes one loopback-only port. Application state and generated
secrets live in a named Docker volume; installer identity and runtime
configuration live in a dedicated private state directory.

## Prerequisites

Before running the installer:

- Install and start Docker from the
  [official Docker Engine](https://docs.docker.com/engine/install/) or
  [Docker Desktop](https://docs.docker.com/desktop/) documentation.
- Select a local Docker endpoint: `unix://` on Linux and macOS, `npipe://` on
  Windows. Remote and TCP contexts are rejected.
- Configure the daemon for Linux containers on `amd64` or `arm64`.
- Allow about 25 GB of free disk; at least 8 GB RAM is recommended.
- Have Bash, curl, and `sha256sum` or `shasum` available for the bootstrap, or
  PowerShell and `curl.exe` on Windows. Docker Compose, Git, Node.js, and npm
  are not required.

Docker Desktop is a separate product with its own license terms. AutoGPT does
not install it or grant a Docker license.

## Install

Each command below uses a unique temporary file, executes it only after the HTTPS
download succeeds, and removes it afterward. Do not stream a network response
directly into a shell.

### Linux and macOS

```bash
(
  installer="$(mktemp)" &&
  trap 'rm -f "$installer"' EXIT &&
  curl --proto '=https' --proto-redir '=https' --tlsv1.2 -fsSL \
    -o "$installer" https://setup.agpt.co/install.sh &&
  bash "$installer"
)
```

### Windows

```powershell
& {
  $installer = Join-Path ([IO.Path]::GetTempPath()) "autogpt-install-$([guid]::NewGuid().ToString('N')).ps1"
  try {
    curl.exe --proto '=https' --proto-redir '=https' --tlsv1.2 -fsSL -o $installer https://setup.agpt.co/install.ps1
    if ($LASTEXITCODE -eq 0) {
      powershell.exe -NoProfile -ExecutionPolicy Bypass -File $installer
    }
  } finally {
    Remove-Item -LiteralPath $installer -Force -ErrorAction SilentlyContinue
  }
}
```

### Options

Add a flag after `bash "$installer"` on Linux and macOS, or after
`-File $installer` on Windows.

| Goal | Linux/macOS | Windows |
| --- | --- | --- |
| Current published appliance | _(no flag)_ | _(no flag)_ |
| Specific published version | `--release=vX.Y.Z` | `-Release vX.Y.Z` |
| Appliance release-tag form | `--release=autogpt-platform-beta-vX.Y.Z` | `-Release autogpt-platform-beta-vX.Y.Z` |
| Hardware and Docker checks only | `--preflight-only` | `-PreflightOnly` |
| Print image selection only | `--resolve-only` | `-ResolveOnly` |
| Skip RAM/disk checks | `--skip-preflight` | `-SkipPreflight` |
| Custom private state directory | `--dir=PATH` | `-Dir PATH` |

`--skip-preflight` does not skip the local-endpoint, Linux-container, or
architecture checks.

The default state directory is `$XDG_CONFIG_HOME/autogpt`, or
`$HOME/.config/autogpt` when `XDG_CONFIG_HOME` is unset, and
`%LOCALAPPDATA%\AutoGPT` on Windows. A custom directory must be user-owned and
private; the installer rejects symlink paths and shared or unsafe locations.

## Artifact and Runtime Contract

With no release flag, the installer pulls
`significantgravitas/autogpt:latest`. An explicit `vX.Y.Z` selects the matching
version tag. After pulling, the installer:

1. Requires the expected OCI title and source repository plus a well-formed
   40-hex source revision label.
2. Requires a Linux image matching the local daemon's `amd64` or `arm64`
   architecture.
3. Resolves exactly one native `RepoDigest` and runs that immutable digest,
   never the mutable tag.
4. Creates an installer-owned, labelled `autogpt-platform-data` volume.
5. Starts `autogpt` with `127.0.0.1:3000`, the private environment file,
   restart and stop policies, log rotation, shared memory, and file-descriptor
   limits.

The bootstrap authenticates transport with HTTPS, and the installer pins the
pulled image by digest after checking its appliance metadata. It does not
independently verify a publisher signature or protect against compromise of
the image-publishing credentials. Signed release artifacts are a follow-up,
not a claim made by this installer.

The installer refuses to adopt an unlabelled container or volume. A rerun is a
no-op, or starts a stopped container, only when the installer identity,
environment hash, immutable image digest, process, port, mount, privilege,
restart, logging, memory, and limit settings all match. Drift fails closed.

If established installer lifecycle state exists but its named volume is
missing, the installer refuses to create an empty replacement. An interrupted
first pull has no established marker and can be retried safely. Restore a
previously established volume or choose a new state directory.

## Runtime Configuration

The private `autogpt.env` initially contains:

```dotenv
AUTOGPT_PUBLIC_URL=http://localhost:3000
```

Add provider keys and other appliance variables there. The installer binds the
environment file's SHA-256 hash to the container label. To apply a deliberate
configuration change, stop and remove only the container, then rerun the same
installer release:

```bash
docker stop --time 360 autogpt
docker rm autogpt
```

Do not remove `autogpt-platform-data`; it contains accounts, agents, generated
secrets, and application state.

## First Run

After the installer reports a healthy container, open
[http://localhost:3000](http://localhost:3000). Registration starts open so
you can create the intended account. Promote it:

```bash
docker exec autogpt autogpt-admin promote you@example.com
```

Then set this in `autogpt.env`, recreate the container as described above, and
verify registration is closed:

```dotenv
AUTH_ALLOW_NEW_ACCOUNTS=false
```

Keep the default loopback binding. To serve other machines, place AutoGPT
behind a TLS reverse proxy and set `AUTOGPT_PUBLIC_URL` to the exact public URL.

## Upgrades

Before an upgrade, back up `autogpt-platform-data` as described in
[Cold backup](single-container.md#cold-backup). Stop and remove only the
`autogpt` container, then rerun the installer with the intended release. The
installer will reuse the named volume only when its ownership labels and the
private installer identity still match.

## Maintainer Release Gates

`https://setup.agpt.co/install.sh`, `https://setup.agpt.co/install.ps1`, and
`https://setup.agpt.co/install.bat` redirect to the files of the same name in
`autogpt_platform/installer/` on `master`, so the hosted installers change
whenever those files change on `master`. Repository CI cannot prove this public
bootstrap handoff, so each release must keep every gate below passing:

- Only a published, non-prerelease `autogpt-platform-beta-vX.Y.Z` release
  publishes images. Its run smoke-tests and scans both architectures, then
  pushes `sha-<commit>` and `vX.Y.Z` and moves `:latest` forward. Pushes to
  `dev` and manual dispatches build, test, and scan without publishing.
- The release's tag commit contains the full installer, publication workflow,
  and helper, and its release-triggered run succeeded.
- Both public tags expose Linux `amd64` and `arm64` manifests and the expected
  OCI identity:

  ```bash
  docker buildx imagetools inspect significantgravitas/autogpt:vX.Y.Z
  docker buildx imagetools inspect significantgravitas/autogpt:latest
  ```

- Fetch each hosted installer into a new temporary file from an external
  machine and compare it with the released repository file. On each supported
  operating system, perform a clean install against the public image, wait for
  health, verify the loopback endpoint, rerun to prove exact-contract
  idempotency, and confirm no mutable tag was used to create the container.

## Installing from source (Compose)

The scripts below are the other supported path: they clone the repository and
start every service with Docker Compose. Use them for development, or for a
fully offline install with a local
LLM. The appliance described above is the image documented in
[Run AutoGPT in one Docker container](single-container.md); these scripts do
not use it.

### Manual Installation

If you prefer, you can manually download and run the installer scripts:

- **Linux/macOS:** `setup-autogpt.sh`
- **Windows:** `setup-autogpt.bat`

These scripts are located in the `autogpt_platform/installer/` directory. Run
`./setup-autogpt.sh` or `setup-autogpt.bat` from there. Inside a clone, a
script uses that checkout; otherwise it clones the repository into a new
`AutoGPT` folder first.

Both create the three `.env` files if they are missing and generate the
secrets `.env.default` leaves blank (`ENCRYPTION_KEY`,
`UNSUBSCRIBE_SECRET_KEY`, `INTERNAL_API_KEY`, `BETTER_AUTH_SECRET`), the same
as `make init-env`.
Running a script again never changes a value that is already set. If you are
upgrading an install that ran without its own `ENCRYPTION_KEY`, read
[Upgrading: secrets are generated per install](getting-started.md#upgrading-secrets-are-generated-per-install)
first so your connected integrations move to the new key.

### Running fully offline with a local LLM (Ollama)

Both installer scripts accept an opt-in flag that installs
[Ollama](https://ollama.com), pulls a default chat model, and wires
`backend/.env` so AutoPilot runs **without any cloud API keys**. This
is useful for air-gapped or privacy-sensitive deployments — see
[Running AutoPilot on a self-hosted LLM](copilot-local-llm.md) for the
full reference.

#### Linux / macOS

```bash
cd autogpt_platform/installer
./setup-autogpt.sh --with-ollama
# Optional overrides:
#   --ollama-model=qwen3:14b-q4_K_M
#   --ollama-host=http://gpu-rig.lab:11434   # use an existing Ollama
```

#### Windows

```cmd
cd autogpt_platform\installer
setup-autogpt.bat /with-ollama
REM Optional overrides:
REM   /ollama-model=qwen3:14b-q4_K_M
REM   /ollama-host=http://gpu-rig.lab:11434
```

The installer:

1. Installs Ollama (skipped if already present, or if `--ollama-host` points at an existing one).
2. Configures `OLLAMA_HOST=0.0.0.0:11434` + `OLLAMA_CONTEXT_LENGTH=262144` so containers can reach it and so AutoPilot's ~8 k system prompt isn't truncated by Ollama's 4 k default; 262,144 tokens is the default model's native window.
3. Pulls the chat model (default `hf.co/ornith-ai/Ornith-1.5-9B-GGUF:Q4_K_M`).
4. Appends a marker-bounded block to `autogpt_platform/backend/.env` with `CHAT_USE_LOCAL=true` plus the `CHAT_BASE_URL` / `CHAT_API_KEY` / `CHAT_*_MODEL` overrides.

Re-running with `--with-ollama` is idempotent — the wiring block is rewritten in place.

## Troubleshooting

1. Confirm `docker info` succeeds against a local Linux-container daemon.
2. If an image pull fails just after release, wait for public manifest
   publication and retry; the installer never falls back to a source build.
3. If the installer reports state or runtime drift, preserve the named volume,
   inspect the existing container, and follow the explicit upgrade procedure.
4. Check `docker logs --tail 100 autogpt` when health checks fail.

---

### Platform Integrations And Credentials (`docs/platform/integrations-and-credentials.md`)

# Integrations & Credentials

## Overview

Many blocks on the AutoGPT Platform integrate with external services like Google, GitHub, Linear, Twitter, and more. These integrations require credentials — such as OAuth connections, API keys, or username/password pairs — to access your accounts on those services.

This guide explains how credentials work on the platform, how to add them, and how to manage them.

## How Credentials Work

### Platform-Provided Credentials

On the cloud-hosted platform at [platform.agpt.co](https://platform.agpt.co), some plans and account configurations include managed AI providers. If a block requires credentials that are not included, the platform will prompt you to connect your own. See the [pricing page](https://agpt.co/pricing) for current plan details.

### User-Provided Credentials

For services tied to your personal accounts, connect your own credentials. Credentials can be reused across agents, but you choose which compatible credential an agent or task uses. Managed credentials may be selected automatically when available.

## Adding Credentials

You can connect credentials centrally from **Settings → Integrations → Connect Service**, or in context when a builder block or task asks for one.

### When Building an Agent

If a block needs credentials, select an existing compatible credential or connect a new one from the block.

### When Running an Agent

Credential fields show the credentials that task will use. Select an existing compatible credential or connect a new one.

### Credential Types

Depending on the service, you'll be prompted to authenticate in one of three ways:

| Type | Description | Example Services |
|------|-------------|------------------|
| **OAuth** | Click to authorise via the service's login page | Google, GitHub, Twitter |
| **API Key** | Paste your API key from the service's dashboard | Linear, OpenAI |
| **Username & Password** | Enter your account credentials | Varies by service |

{% hint style="info" %}
Credentials are reusable, but each block or task retains its selected credential reference.
{% endhint %}

## Connecting MCP Services

Open **Settings → Integrations**, find a service with the **MCP** badge, and follow its setup instructions. The same services appear in **Connect Service**.

- Choose the offered sign-in or token option for account access, or **No sign-in → Check connection** for public tools.
- If prompted, select your account's region or enter the official server URL for your tenant or deployment.
- Review the requested permissions. **Allow changes**, when offered, adds the service's optional actions.

For catalog updates, see [Maintaining MCP Integrations](https://github.com/Significant-Gravitas/AutoGPT/blob/dev/docs/engineering/hosted-mcp-catalog.md).

## Managing Credentials

1. Open **Settings**.
2. Select **Integrations**.
3. Use **Connect Service** to add a credential, or manage/remove credentials already listed.

**URL:** [platform.agpt.co/settings/integrations](https://platform.agpt.co/settings/integrations)

{% hint style="warning" %}
Removing a credential can break agents, workflows, or active webhooks that reference it. Review the dependency warning before confirming removal.
{% endhint %}

## Self-Hosted Credentials

When self-hosting, configure deployment-level provider keys and OAuth application credentials as environment variables. For the [Docker Hub image](single-container.md), put them in the environment file you pass to `docker run --env-file`; for a Docker Compose or source install, put them in `autogpt_platform/backend/.env`. Register `<your AutoGPT URL>/auth/integrations/oauth_callback` (for example `http://localhost:3000/auth/integrations/oauth_callback`) as each OAuth app's redirect URI, then connect end-user accounts from **Settings → Integrations** as needed. See the [Self-Hosting Guide](getting-started.md) for details.

---

### Platform Marketplace (`docs/platform/marketplace.md`)

# Marketplace

## Overview

The AutoGPT Marketplace is a public library of agents created by the community and by the AutoGPT team. You can discover agents for a wide range of use cases, add them to your library with one click, and even publish your own agents for others to use.

**URL:** [platform.agpt.co/marketplace](https://platform.agpt.co/marketplace)

**Access:** Click **Marketplace** in the navigation bar.

## Browsing the Marketplace

The marketplace allows you to:

- **Search** for agents by name or keyword
- **Browse by category** to explore different use cases
- **View agent details** by clicking on any agent

### Agent Detail Page

Each agent in the marketplace has a dedicated page that includes:

- **Title and description** of what the agent does
- **Creator information** — who built the agent and a link to their other agents
- **Video** (when available) — a demonstration of the agent in action
- **Output showcase** (when available) — examples of the agent's results
- **Instructions** — guidance on how to run the agent and what to expect

## Adding an Agent to Your Library

When you find an agent you want to use:

1. Click on the agent to open its marketplace page
2. Click the **Add to Library** button

The agent appears in your [Agent Library](agent-library.md), where you can run or schedule it. Marketplace agents open read-only; duplicate one before editing.

{% hint style="info" %}
If you already have the agent in your library, the button changes to **See Runs**, which takes you directly to that agent in your library.
{% endhint %}

### Download for Self-Hosting

Marketplace detail pages also provide **Download here** under “Want to use this agent locally?” Download the agent JSON and import it into your local instance. See [Download & Import an Agent](download-agent-from-marketplace-local.md) for details.

## Publishing an Agent

You can publish your own agents to the marketplace for the community to discover and use.

### How to Publish

1. Open the agent in the Builder and select **Publish to Marketplace**, or open **Settings → Creator Dashboard** and select **Publish Agent**
2. Choose the agent and version you want to publish if prompted
3. Fill out the publishing form:

| Field | Description |
|-------|-------------|
| **Title** | The name of your agent as it will appear in the marketplace |
| **Subheader** | A short tagline for your agent |
| **Slug** | A URL-friendly identifier (e.g., `resume-rewriter`) |
| **Thumbnail Images** | Upload images — the first image becomes the marketplace thumbnail |
| **YouTube Video Link** | Optional — a video demonstrating your agent |
| **Output demo** | Optional — a URL showing example output the agent produces |
| **Category** | Select the most relevant category for your agent |
| **Description** | A detailed explanation of what your agent does |
| **Instructions** | Explain to users how to run this agent and what to expect |
| **Recommended Schedule** | Suggest when users should run this agent for best results |

4. Click **Submit for review**

### Review Process

After submission, a member of the AutoGPT team will review your agent against curation standards. If approved, your agent will be published to the marketplace and visible to all users.

## Editing Marketplace Agents

An agent added from the marketplace is read-only. Open it in the builder and click **Duplicate** to create an editable fork in your library. Changes to that fork do not modify the original marketplace listing.

---

### Platform New Blocks (`docs/platform/new_blocks.md`)

# Contributing to AutoGPT Agent Server: Creating and Testing Blocks

This guide will walk you through the process of creating and testing a new block for the AutoGPT Agent Server, using the WikipediaSummaryBlock as an example.

!!! tip "New SDK-Based Approach"
For a more comprehensive guide using the new SDK pattern with ProviderBuilder and advanced features like OAuth and webhooks, see the [Block SDK Guide](block-sdk-guide.md).

## Understanding Blocks and Testing

Blocks are reusable components that can be connected to form a graph representing an agent's behavior. Each block has inputs, outputs, and a specific function. Proper testing is crucial to ensure blocks work correctly and consistently.

## Block Auto-Discovery

All block modules are **automatically discovered and imported** at startup. The block loader in `backend/blocks/__init__.py` uses `Path(__file__).parent.rglob("*.py")` to find every `.py` file in the `blocks/` directory tree. Files named `__init__.py` and those starting with `test_` are excluded.

This means:

- You do **not** need to manually register your block anywhere — just create the file and your `Block` subclass will be picked up automatically.
- Any **module-level side effects** (e.g., global registrations, print statements) in your block files will execute on import. Keep block packages clean and avoid side-effect-heavy code at module scope.
- Block auto-discovery does not replace provider setup. For a new API-key integration, follow the [provider auth setup](#api-key-only-provider-auth-recommended-pattern) below: add the provider name, credentials, display metadata, and [integration icon](#integration-icon-required). Frontend provider data is loaded dynamically; no manual frontend provider registration is needed.

## Creating and Testing a New Block

Follow these steps to create and test a new block:

1. **Create a new Python file** for your block in the `autogpt_platform/backend/backend/blocks` directory. Name it descriptively and use snake_case. For example: `get_wikipedia_summary.py`.

2. **Import necessary modules and create a class that inherits from `Block`**. Make sure to include all necessary imports for your block.

   Every block should contain the following:

   ```python
   from backend.blocks._base import Block, BlockSchemaInput, BlockSchemaOutput, BlockOutput
   ```

   Example for the Wikipedia summary block:

   ```python
   from backend.blocks._base import Block, BlockSchemaInput, BlockSchemaOutput, BlockOutput
   from backend.utils.get_request import GetRequest
   import requests

   class WikipediaSummaryBlock(Block, GetRequest):
       # Block implementation will go here
   ```

3. **Define the input and output schemas** using `BlockSchema`. These schemas specify the data structure that the block expects to receive (input) and produce (output).

   - The input schema defines the structure of the data the block will process. Each field in the schema represents a required piece of input data.
   - The output schema defines the structure of the data the block will return after processing. Each field in the schema represents a piece of output data.

   Example:

   ```python
   class Input(BlockSchemaInput):
       topic: str  # The topic to get the Wikipedia summary for

   class Output(BlockSchemaOutput):
       summary: str  # The summary of the topic from Wikipedia
   ```

4. **Implement the `__init__` method, including test data and mocks:**

   !!! important
   Use UUID generator (e.g. https://www.uuidgenerator.net/) for every new block `id` and _do not_ make up your own. Alternatively, you can run this python code to generate an uuid: `print(__import__('uuid').uuid4())`

   ```python
   def __init__(self):
       super().__init__(
           # Unique ID for the block, used across users for templates
           # If you are an AI leave it as is or change to "generate-proper-uuid"
           id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
           input_schema=WikipediaSummaryBlock.Input,  # Assign input schema
           output_schema=WikipediaSummaryBlock.Output,  # Assign output schema

               # Provide sample input, output and test mock for testing the block

           test_input={"topic": "Artificial Intelligence"},
           test_output=("summary", "summary content"),
           test_mock={"get_request": lambda url, json: {"extract": "summary content"}},
       )
   ```

   - `id`: A unique identifier for the block.

   - `input_schema` and `output_schema`: Define the structure of the input and output data.

   Let's break down the testing components:

   - `test_input`: This is a sample input that will be used to test the block. It should be a valid input according to your Input schema.

   - `test_output`: This is the expected output when running the block with the `test_input`. It should match your Output schema. For non-deterministic outputs or when you only want to assert the type, you can use Python types instead of specific values. In this example, `("summary", str)` asserts that the output key is "summary" and its value is a string.

   - `test_mock`: This is crucial for blocks that make network calls. It provides a mock function that replaces the actual network call during testing.

   In this case, we're mocking the `get_request` method to always return a dictionary with an 'extract' key, simulating a successful API response. This allows us to test the block's logic without making actual network requests, which could be slow, unreliable, or rate-limited.

5. **Implement the `run` method with error handling.** This should contain the main logic of the block:

   ```python
   def run(self, input_data: Input, **kwargs) -> BlockOutput:
       try:
           topic = input_data.topic
           url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{topic}"

           response = self.get_request(url, json=True)
           yield "summary", response['extract']

       except requests.exceptions.HTTPError as http_err:
           raise RuntimeError(f"HTTP error occurred: {http_err}")
   ```

   - **Try block**: Contains the main logic to fetch and process the Wikipedia summary.
   - **API request**: Send a GET request to the Wikipedia API.
   - **Error handling**: Handle various exceptions that might occur during the API request and data processing. We don't need to catch all exceptions, only the ones we expect and can handle. The uncaught exceptions will be automatically yielded as `error` in the output. Any block that raises an exception (or yields an `error` output) will be marked as failed. Prefer raising exceptions over yielding `error`, as it will stop the execution immediately.
   - **Yield**: Use `yield` to output the results. Prefer to output one result object at a time. If you are calling a function that returns a list, you can yield each item in the list separately. You can also yield the whole list as well, but do both rather than yielding the list. For example: If you were writing a block that outputs emails, you'd yield each email as a separate result object, but you could also yield the whole list as an additional single result object. Yielding output named `error` will break the execution right away and mark the block execution as failed.
   - **kwargs**: The `kwargs` parameter is used to pass additional arguments to the block. It is not used in the example above, but it is available to the block. You can also have args as inline signatures in the run method ala `def run(self, input_data: Input, *, user_id: str, **kwargs) -> BlockOutput:`.
     Available kwargs are:
     - `user_id`: The ID of the user running the block.
     - `graph_id`: The ID of the agent that is executing the block. This is the same for every version of the agent
     - `graph_exec_id`: The ID of the execution of the agent. This changes every time the agent has a new "run"
     - `node_exec_id`: The ID of the execution of the node. This changes every time the node is executed
     - `node_id`: The ID of the node that is being executed. It changes every version of the graph, but not every time the node is executed.
     - `execution_context`: An `ExecutionContext` object containing user_id, graph_exec_id, workspace_id, and session_id. Required for file handling.

### Handling Files in Blocks

When your block needs to work with files (images, videos, documents), use `store_media_file()` from `backend.util.file`. This function handles downloading, validation, virus scanning, and storage.

**Import:**
```python
from backend.data.execution import ExecutionContext
from backend.util.file import store_media_file
from backend.util.type import MediaFileType
```

**The `return_format` parameter determines what you get back:**

| Format | Use When | Returns |
|--------|----------|---------|
| `"for_local_processing"` | Processing with local tools (ffmpeg, MoviePy, PIL) | Local file path (e.g., `"image.png"`) |
| `"for_external_api"` | Sending content to external APIs (Replicate, OpenAI) | Data URI (e.g., `"data:image/png;base64,..."`) |
| `"for_block_output"` | Returning output from your block | Smart: `workspace://` in CoPilot, data URI in graphs |

**Examples:**

```python
async def run(
    self,
    input_data: Input,
    *,
    execution_context: ExecutionContext,
    **kwargs,
) -> BlockOutput:
    # PROCESSING: Need to work with file locally (ffmpeg, MoviePy, PIL)
    local_path = await store_media_file(
        file=input_data.video,
        execution_context=execution_context,
        return_format="for_local_processing",
    )
    # local_path = "video.mp4" - use with Path, ffmpeg, subprocess, etc.
    full_path = get_exec_file_path(execution_context.graph_exec_id, local_path)

    # EXTERNAL API: Need to send content to an API like Replicate
    image_b64 = await store_media_file(
        file=input_data.image,
        execution_context=execution_context,
        return_format="for_external_api",
    )
    # image_b64 = "data:image/png;base64,iVBORw0..." - send to external API

    # OUTPUT: Returning result from block to user/next block
    result_url = await store_media_file(
        file=generated_image_url,
        execution_context=execution_context,
        return_format="for_block_output",
    )
    yield "image_url", result_url
    # In CoPilot: result_url = "workspace://abc123" (persistent, context-efficient)
    # In graphs:  result_url = "data:image/png;base64,..." (for next block/display)
```

**Key points:**

- `for_block_output` is the **only** format that auto-adapts to execution context
- Always use `for_block_output` for block outputs unless you have a specific reason not to
- Never manually check for `workspace_id` - let `for_block_output` handle the logic
- The function handles URLs, data URIs, `workspace://` references, and local paths as input

### Field Types

#### oneOf fields

`oneOf` allows you to specify that a field must be exactly one of several possible options. This is useful when you want your block to accept different types of inputs that are mutually exclusive.

Example:

```python
attachment: Union[Media, DeepLink, Poll, Place, Quote] = SchemaField(
    discriminator='discriminator',
    description="Attach either media, deep link, poll, place or quote - only one can be used"
)
```

The `discriminator` parameter tells AutoGPT which field to look at in the input to determine which type it is.

In each model, you need to define the discriminator value:

```python
class Media(BaseModel):
    discriminator: Literal['media']
    media_ids: List[str]

class DeepLink(BaseModel):
    discriminator: Literal['deep_link']
    direct_message_deep_link: str
```

#### OptionalOneOf fields

`OptionalOneOf` is similar to `oneOf` but allows the field to be optional (None). This means the field can be either one of the specified types or None.

Example:

```python
attachment: Union[Media, DeepLink, Poll, Place, Quote] | None = SchemaField(
    discriminator='discriminator',
    description="Optional attachment - can be media, deep link, poll, place, quote or None"
)
```

The key difference is the `| None` which makes the entire field optional.

### Blocks with authentication

Our system supports auth offloading for API keys and OAuth2 authorization flows.
Adding a block with API key authentication is straight-forward, as is adding a block
for a service that we already have OAuth2 support for.

Implementing the block itself is relatively simple. On top of the instructions above,
you're going to add a `credentials` parameter to the `Input` model and the `run` method:

```
/* Detailed source-code truncated for AI context efficiency. */
```

The credentials will be automagically injected by the executor in the back end.

The `APIKeyCredentials` and `OAuth2Credentials` models are defined [here](https://github.com/Significant-Gravitas/AutoGPT/blob/master/autogpt_platform/backend/backend/data/model.py).
To use them in e.g. an API request, you can either access the token directly:

```python
# credentials: APIKeyCredentials
response = requests.post(
    url,
    headers={
        "Authorization": f"Bearer {credentials.api_key.get_secret_value()})",
    },
)

# credentials: OAuth2Credentials
response = requests.post(
    url,
    headers={
        "Authorization": f"Bearer {credentials.access_token.get_secret_value()})",
    },
)
```

or use the shortcut `credentials.auth_header()`:

```python
# credentials: APIKeyCredentials | OAuth2Credentials
response = requests.post(
    url,
    headers={"Authorization": credentials.auth_header()},
)
```

The backend discovers provider names from the `ProviderName` enum and the SDK's `AutoRegistry`.
The `_auth.py` pattern below uses a `ProviderName` member, so add the new provider to that enum. SDK integrations can instead register provider names through `ProviderBuilder`; see the [Block SDK Guide](block-sdk-guide.md).

<details>
<summary><code>ProviderName</code> definition</summary>

```python title="backend/integrations/providers.py"
--8<-- "autogpt_platform/backend/backend/integrations/providers.py:ProviderName"
```

</details>

#### API-Key-Only Provider Auth (Recommended Pattern)

For integrations that only use API key authentication (no OAuth), keep credential definitions in `_auth.py` and register display metadata separately. The setup is:

1. **Add to `ProviderName` enum** in `backend/integrations/providers.py`
2. **Create `_auth.py`** in your block package with credentials type, field factory, and test credentials
3. **Create the block** using the credentials field from `_auth.py`
4. **Create `_config.py`** with the provider description and supported auth types for the settings UI
5. **Add the integration icon** as described [below](#integration-icon-required)

Here's the complete `_auth.py` pattern (using ZeroBounce as a reference):

```python title="backend/blocks/my_provider/_auth.py"
from typing import Literal

from pydantic import SecretStr

from backend.data.model import APIKeyCredentials, CredentialsField, CredentialsMetaInput
from backend.integrations.providers import ProviderName

# Type aliases for your provider
MyProviderCredentials = APIKeyCredentials
MyProviderCredentialsInput = CredentialsMetaInput[
    Literal[ProviderName.MY_PROVIDER],
    Literal["api_key"],
]


# Field factory for use in block Input schemas
def MyProviderCredentialsField() -> MyProviderCredentialsInput:
    return CredentialsField(
        description="The My Provider integration requires an API key.",
    )


# Test credentials for block tests
TEST_CREDENTIALS = APIKeyCredentials(
    id="01234567-89ab-cdef-0123-456789abcdef",
    provider="my_provider",
    api_key=SecretStr("mock-my-provider-api-key"),
    title="Mock My Provider API key",
    expires_at=None,
)

TEST_CREDENTIALS_INPUT = {
    "provider": TEST_CREDENTIALS.provider,
    "id": TEST_CREDENTIALS.id,
    "type": TEST_CREDENTIALS.type,
    "title": TEST_CREDENTIALS.title,
}
```

Then use it in your block:

```python title="backend/blocks/my_provider/my_block.py"
from ._auth import MyProviderCredentials, MyProviderCredentialsField, MyProviderCredentialsInput

class MyBlock(Block):
    class Input(BlockSchemaInput):
        credentials: MyProviderCredentialsInput = MyProviderCredentialsField()
        # ... other inputs

    async def run(
        self,
        input_data: Input,
        *,
        credentials: MyProviderCredentials,
        **kwargs,
    ) -> BlockOutput:
        api_key = credentials.api_key.get_secret_value()
        # ... use the API key
        yield "result", ...
```

Register the provider metadata in `_config.py`, following `backend/blocks/zerobounce/_config.py`:

```python title="backend/blocks/my_provider/_config.py"
from backend.integrations.providers import ProviderName
from backend.sdk import ProviderBuilder

my_provider = (
    ProviderBuilder(ProviderName.MY_PROVIDER.value)
    .with_description("A short description of the integration")
    .with_supported_auth_types("api_key")
    .build()
)
```

This metadata tells the settings UI which connection methods to offer. It does not replace the credentials declared in `_auth.py`. The block loader imports `_config.py` automatically, and `.build()` registers the provider. OAuth, webhooks, and cost tracking use additional `ProviderBuilder` methods described in the [Block SDK Guide](block-sdk-guide.md).

#### Multiple credentials inputs

Multiple credentials inputs are supported, under the following conditions:

- The name of each of the credentials input fields must end with `_credentials`.
- The names of the credentials input fields must match the names of the corresponding
  parameters on the `run(..)` method of the block.
- If more than one of the credentials parameters are required, `test_credentials`
  is a `dict[str, Credentials]`, with for each required credentials input the
  parameter name as the key and suitable test credentials as the value.

#### Adding an OAuth2 service integration

To add support for a new OAuth2-authenticated service, you'll need to add an `OAuthHandler`.
All our existing handlers and the base class can be found [here][OAuth2 handlers].

Every handler must implement the following parts of the [`BaseOAuthHandler`] interface:

```python title="backend/integrations/oauth/base.py"
--8<-- "autogpt_platform/backend/backend/integrations/oauth/base.py:BaseOAuthHandler1"
--8<-- "autogpt_platform/backend/backend/integrations/oauth/base.py:BaseOAuthHandler2"
--8<-- "autogpt_platform/backend/backend/integrations/oauth/base.py:BaseOAuthHandler3"
--8<-- "autogpt_platform/backend/backend/integrations/oauth/base.py:BaseOAuthHandler4"
--8<-- "autogpt_platform/backend/backend/integrations/oauth/base.py:BaseOAuthHandler5"
--8<-- "autogpt_platform/backend/backend/integrations/oauth/base.py:BaseOAuthHandler6"
```

As you can see, this is modeled after the standard OAuth2 flow.

Aside from implementing the `OAuthHandler` itself, adding a handler into the system requires two more things:

- Adding the handler class to `HANDLERS_BY_NAME` under [`integrations/oauth/__init__.py`](https://github.com/Significant-Gravitas/AutoGPT/blob/master/autogpt_platform/backend/backend/integrations/oauth/__init__.py)

```python title="backend/integrations/oauth/__init__.py"
--8<-- "autogpt_platform/backend/backend/integrations/oauth/__init__.py:HANDLERS_BY_NAMEExample"
```

- Adding `{provider}_client_id` and `{provider}_client_secret` to the application's `Secrets` under [`util/settings.py`](https://github.com/Significant-Gravitas/AutoGPT/blob/master/autogpt_platform/backend/backend/util/settings.py)

```python title="backend/util/settings.py"
--8<-- "autogpt_platform/backend/backend/util/settings.py:OAuthServerCredentialsExample"
```

[OAuth2 handlers]: https://github.com/Significant-Gravitas/AutoGPT/tree/master/autogpt_platform/backend/backend/integrations/oauth
[`BaseOAuthHandler`]: https://github.com/Significant-Gravitas/AutoGPT/blob/master/autogpt_platform/backend/backend/integrations/oauth/base.py

#### Adding to the frontend

##### Integration Icon (Required)

When adding a new integration provider, you **must** add a PNG icon at:

```text
autogpt_platform/frontend/public/integrations/{provider_name}.png
```

The frontend block menu dynamically loads icons using the URL `/integrations/{integration.name}.png` (see `PaginatedIntegrationList.tsx`). If the icon file is missing, the block will show a **broken image** in the builder UI.

Use the same `provider_name` that matches your `ProviderName` enum value (lowercase). For example, if your provider is `ProviderName.SLACK`, the icon should be at `frontend/public/integrations/slack.png`.

##### Frontend Provider Registration

No manual frontend provider registration is required. The credentials provider context fetches provider names from the backend, and the integrations UI uses the description and `supported_auth_types` returned by `GET /integrations/providers`. Set those fields in the backend provider configuration as shown above.

Block credential inputs derive their accepted credential types from the block's input schema. `CredentialsProviderName` is a string type; `CredentialsType` describes authentication methods, not provider names, so neither needs a new frontend enum entry for a provider.

The credentials list has an optional `providerIcons` map in `frontend/src/components/contextual/CredentialsInput/helpers.ts`; unlisted providers use a fallback icon. This is separate from the PNG asset required by the block menu.

#### Example: GitHub integration

- GitHub blocks with API key + OAuth2 support: [`blocks/github`](https://github.com/Significant-Gravitas/AutoGPT/tree/master/autogpt_platform/backend/backend/blocks/github/)

```python title="backend/blocks/github/issues.py"
--8<-- "autogpt_platform/backend/backend/blocks/github/issues.py:GithubCommentBlockExample"
```

- GitHub OAuth2 handler: [`integrations/oauth/github.py`](https://github.com/Significant-Gravitas/AutoGPT/blob/master/autogpt_platform/backend/backend/integrations/oauth/github.py)

```python title="backend/integrations/oauth/github.py"
--8<-- "autogpt_platform/backend/backend/integrations/oauth/github.py:GithubOAuthHandlerExample"
```

#### Example: Google integration

- Google OAuth2 handler: [`integrations/oauth/google.py`](https://github.com/Significant-Gravitas/AutoGPT/blob/master/autogpt_platform/backend/backend/integrations/oauth/google.py)

```python title="backend/integrations/oauth/google.py"
--8<-- "autogpt_platform/backend/backend/integrations/oauth/google.py:GoogleOAuthHandlerExample"
```

You can see that google has defined a `DEFAULT_SCOPES` variable, this is used to set the scopes that are requested no matter what the user asks for.

```python title="backend/blocks/google/_auth.py"
--8<-- "autogpt_platform/backend/backend/blocks/google/_auth.py:GoogleOAuthIsConfigured"
```

You can also see that `GOOGLE_OAUTH_IS_CONFIGURED` is used to disable the blocks that require OAuth if the oauth is not configured. This is in the `__init__` method of each block. This is because there is no api key fallback for google blocks so we need to make sure that the oauth is configured before we allow the user to use the blocks.

### Webhook-triggered Blocks

Webhook-triggered blocks allow your agent to respond to external events in real-time.
These blocks are triggered by incoming webhooks from third-party services
rather than being executed manually.

Creating and running a webhook-triggered block involves three main components:

- The block itself, which specifies:
  - Inputs for the user to select a resource and events to subscribe to
  - A `credentials` input with the scopes needed to manage webhooks
  - Logic to turn the webhook payload into outputs for the webhook block
- The `WebhooksManager` for the corresponding webhook service provider, which handles:
  - (De)registering webhooks with the provider
  - Parsing and validating incoming webhook payloads
- The credentials system for the corresponding service provider, which may include an `OAuthHandler`

There is more going on under the hood, e.g. to store and retrieve webhooks and their
links to nodes, but to add a webhook-triggered block you shouldn't need to make changes
to those parts of the system.

#### Creating a Webhook-triggered Block

To create a webhook-triggered block, follow these additional steps on top of the basic block creation process:

1. **Define `webhook_config`** in your block's `__init__` method.

   <details>
   <summary>Example: <code>GitHubPullRequestTriggerBlock</code></summary>

   ```python title="backend/blocks/github/triggers.py"
   --8<-- "autogpt_platform/backend/backend/blocks/github/triggers.py:example-webhook_config"
   ```

   </details>

   <details>
   <summary><code>BlockWebhookConfig</code> definition</summary>

   ```python title="backend/blocks/_base.py"
   --8<-- "autogpt_platform/backend/backend/blocks/_base.py:BlockWebhookConfig"
   ```

   </details>

2. **Define event filter input** in your block's Input schema.
   This allows the user to select which specific types of events will trigger the block in their agent.

   <details>
   <summary>Example: <code>GitHubPullRequestTriggerBlock</code></summary>

   ```python title="backend/blocks/github/triggers.py"
   --8<-- "autogpt_platform/backend/backend/blocks/github/triggers.py:example-event-filter"
   ```

   </details>

   - The name of the input field (`events` in this case) must match `webhook_config.event_filter_input`.
   - The event filter itself must be a Pydantic model with only boolean fields.

3. **Include payload field** in your block's Input schema.

   <details>
   <summary>Example: <code>GitHubTriggerBase</code></summary>

   ```python title="backend/blocks/github/triggers.py"
   --8<-- "autogpt_platform/backend/backend/blocks/github/triggers.py:example-payload-field"
   ```

   </details>

4. **Define `credentials` input** in your block's Input schema.

   - Its scopes must be sufficient to manage a user's webhooks through the provider's API
   - See [Blocks with authentication](#blocks-with-authentication) for further details

5. **Process webhook payload** and output relevant parts of it in your block's `run` method.

   <details>
   <summary>Example: <code>GitHubPullRequestTriggerBlock</code></summary>

   ```python
   def run(self, input_data: Input, **kwargs) -> BlockOutput:
       yield "payload", input_data.payload
       yield "sender", input_data.payload["sender"]
       yield "event", input_data.payload["action"]
       yield "number", input_data.payload["number"]
       yield "pull_request", input_data.payload["pull_request"]
   ```

   Note that the `credentials` parameter can be omitted if the credentials
   aren't used at block runtime, like in the example.
   </details>

#### Adding a Webhooks Manager

To add support for a new webhook provider, you'll need to create a WebhooksManager that implements the `BaseWebhooksManager` interface:

```python title="backend/integrations/webhooks/_base.py"
--8<-- "autogpt_platform/backend/backend/integrations/webhooks/_base.py:BaseWebhooksManager1"

--8<-- "autogpt_platform/backend/backend/integrations/webhooks/_base.py:BaseWebhooksManager2"
--8<-- "autogpt_platform/backend/backend/integrations/webhooks/_base.py:BaseWebhooksManager3"
--8<-- "autogpt_platform/backend/backend/integrations/webhooks/_base.py:BaseWebhooksManager4"
--8<-- "autogpt_platform/backend/backend/integrations/webhooks/_base.py:BaseWebhooksManager5"
--8<-- "autogpt_platform/backend/backend/integrations/webhooks/_base.py:BaseWebhooksManager6"
```

!!! info "Signature verification (`verify_signature`)"
    If the upstream provider signs its deliveries (e.g. GitHub's
    `X-Hub-Signature-256`, Airtable's `X-Airtable-Content-MAC`), override
    `verify_signature` and use `hmac.compare_digest` for the comparison.
    Raise `fastapi.HTTPException(403)` on missing/invalid signatures.

    If the provider has no signing scheme (consumer wearables, simple
    "POST to this URL" tools), leave the default in place. The webhook
    URL's UUID is then the bearer secret — document that for users.

And add a reference to your `WebhooksManager` class in `load_webhook_managers`:

```python title="backend/integrations/webhooks/__init__.py"
--8<-- "autogpt_platform/backend/backend/integrations/webhooks/__init__.py:load_webhook_managers"
```

#### Example: GitHub Webhook Integration

<details>
<summary>
GitHub Webhook triggers: <a href="https://github.com/Significant-Gravitas/AutoGPT/blob/master/autogpt_platform/backend/backend/blocks/github/triggers.py"><code>blocks/github/triggers.py</code></a>
</summary>

```python title="backend/blocks/github/triggers.py"
--8<-- "autogpt_platform/backend/backend/blocks/github/triggers.py:GithubTriggerExample"
```

</details>

<details>
<summary>
GitHub Webhooks Manager: <a href="https://github.com/Significant-Gravitas/AutoGPT/blob/master/autogpt_platform/backend/backend/integrations/webhooks/github.py"><code>integrations/webhooks/github.py</code></a>
</summary>

```python title="backend/integrations/webhooks/github.py"
--8<-- "autogpt_platform/backend/backend/integrations/webhooks/github.py:GithubWebhooksManager"
```

</details>

## Key Points to Remember

- **Unique ID**: Give your block a unique ID in the **init** method.
- **Input and Output Schemas**: Define clear input and output schemas.
- **Error Handling**: Implement error handling in the `run` method.
- **Output Results**: Use `yield` to output results in the `run` method.
- **Testing**: Provide test input and output in the **init** method for automatic testing.

## Understanding the Testing Process

The testing of blocks is handled by `test_block.py`, which does the following:

1. It calls the block with the provided `test_input`.
   If the block has a `credentials` field, `test_credentials` is passed in as well.
2. If a `test_mock` is provided, it temporarily replaces the specified methods with the mock functions.
3. It then asserts that the output matches the `test_output`.

For the WikipediaSummaryBlock:

- The test will call the block with the topic "Artificial Intelligence".
- Instead of making a real API call, it will use the mock function, which returns `{"extract": "summary content"}`.
- It will then check if the output key is "summary" and its value is a string.

This approach allows us to test the block's logic comprehensively without relying on external services, while also accommodating non-deterministic outputs.

## Security Best Practices for SSRF Prevention

When creating blocks that handle external URL inputs or make network requests, it's crucial to use the platform's built-in SSRF protection mechanisms. The `backend.util.request` module provides a secure `Requests` wrapper class that should be used for all HTTP requests.

### Using the Secure Requests Wrapper

```python
from backend.util.request import requests

class MyNetworkBlock(Block):
    def run(self, input_data: Input, **kwargs) -> BlockOutput:
        try:
            # The requests wrapper automatically validates URLs and blocks dangerous requests
            response = requests.get(input_data.url)
            yield "result", response.text
        except ValueError as e:
            # URL validation failed
            raise RuntimeError(f"Invalid URL provided: {e}")
        except requests.exceptions.RequestException as e:
            # Request failed
            raise RuntimeError(f"Request failed: {e}")
```

The `Requests` wrapper provides these security features:

1. **URL Validation**:

   - Blocks requests to private IP ranges (RFC 1918)
   - Validates URL format and protocol
   - Resolves DNS and checks IP addresses
   - Supports whitelisting trusted origins

2. **Secure Defaults**:

   - Disables redirects by default
   - Raises exceptions for non-200 status codes
   - Supports custom headers and validators

3. **Protected IP Ranges**:
   The wrapper denies requests to these networks:

   ```python title="backend/util/request.py"
   --8<-- "autogpt_platform/backend/backend/util/request.py:BLOCKED_IP_NETWORKS"
   ```

### Custom Request Configuration

If you need to customize the request behavior:

```python
from backend.util.request import Requests

# Create a custom requests instance with specific trusted origins
custom_requests = Requests(
    trusted_origins=["api.trusted-service.com"],
    raise_for_status=True,
    extra_headers={"User-Agent": "MyBlock/1.0"}
)
```

### Error Handling

Blocks should raise appropriate exceptions for errors that users can fix. The executor classifies errors based on whether they inherit from `ValueError` - these are treated as "expected failures" (user-fixable) rather than system errors.

#### Block Exception Classes

Import from `backend.util.exceptions`:

```python
from backend.util.exceptions import BlockInputError, BlockExecutionError
```

| Exception | Use Case | Example |
|-----------|----------|---------|
| `BlockInputError` | Invalid user input, validation failures, missing required fields | Bad API key format, invalid URL, missing credentials |
| `BlockExecutionError` | Runtime failures the user can address | API errors, auth failures, resource not found, rate limits |
| `ValueError` | Simple cases (auto-wrapped to `BlockExecutionError`) | Basic validation errors |

#### Raising Exceptions

```python
from backend.util.exceptions import BlockInputError, BlockExecutionError

class MyBlock(Block):
    async def run(self, input_data: Input, **kwargs) -> BlockOutput:
        # Input validation - use BlockInputError
        if not input_data.api_key:
            raise BlockInputError(
                message="API key is required",
                block_name=self.name,
                block_id=self.id,
            )

        try:
            result = await self.call_api(input_data)
            yield "result", result
        except AuthenticationError as e:
            # API/runtime errors - use BlockExecutionError
            raise BlockExecutionError(
                message=f"Authentication failed: {e}",
                block_name=self.name,
                block_id=self.id,
            ) from e
```

#### What NOT to Catch

Don't catch errors that require system admin intervention:

- Out of money/credits
- Unreachable infrastructure
- Database connection failures
- Internal server errors from your own services

Let these propagate as unexpected errors so they get proper attention.  

### Data Models

Use pydantic base models over dict and typeddict where possible. Avoid untyped models for block inputs and outputs as much as possible

### File Input

You can use MediaFileType to handle the importing and exporting of files out of the system. Explore how its used through the system before using it in a block schema.

## Tips for Effective Block Testing

1. **Provide realistic test_input**: Ensure your test input covers typical use cases.

2. **Define appropriate test_output**:

   - For deterministic outputs, use specific expected values.
   - For non-deterministic outputs or when only the type matters, use Python types (e.g., `str`, `int`, `dict`).
   - You can mix specific values and types, e.g., `("key1", str), ("key2", 42)`.

3. **Use test_mock for network calls**: This prevents tests from failing due to network issues or API changes.

4. **Consider omitting test_mock for blocks without external dependencies**: If your block doesn't make network calls or use external resources, you might not need a mock.

5. **Consider edge cases**: Include tests for potential error conditions in your `run` method.

6. **Update tests when changing block behavior**: If you modify your block, ensure the tests are updated accordingly.

By following these steps, you can create new blocks that extend the functionality of the AutoGPT Agent Server.

## Standalone Unit Tests

In addition to the inline `test_input`/`test_output`/`test_mock` approach described above, you can write standalone unit tests for more complex validation. Place test files alongside your block as `{block_name}_test.py` (e.g., `my_block_test.py`).

!!! warning "Test file naming and auto-discovery"
    The block loader in `backend/blocks/__init__.py` skips files whose names start with `test_` but still auto-imports files ending in `_test.py` at startup. Keep `_test.py` modules free of module-level side effects (no top-level `patch(...)` / network calls / fixtures-with-side-effects), or name your file `test_{block_name}.py` to opt out of auto-import entirely.

### Test Structure

Two patterns are supported:

**1. Drive the block directly** (mirror `backend/blocks/slack/slack_test.py`) — gives you full control over inputs, mocks, and output assertions:

```python title="backend/blocks/my_provider/my_block_test.py"
import pytest
from unittest.mock import AsyncMock, MagicMock, patch

from backend.blocks.my_provider._auth import TEST_CREDENTIALS, TEST_CREDENTIALS_INPUT
from backend.blocks.my_provider.my_block import MyBlock


async def _collect_outputs(block, input_data, credentials):
    outputs: dict[str, object] = {}
    async for name, value in block.run(input_data, credentials=credentials):
        outputs[name] = value
    return outputs


@pytest.mark.asyncio
async def test_my_block_success():
    block = MyBlock()
    input_data = MyBlock.Input(
        credentials=TEST_CREDENTIALS_INPUT,
        query="test query",
    )
    mock_resp = MagicMock()
    mock_resp.json.return_value = {"result": "test data"}

    # Patch where the symbol is *used*. The block ecosystem uses
    # `backend.util.request.Requests` (httpx under the hood), not stdlib `requests`.
    with patch(
        "backend.blocks.my_provider.my_block.Requests.post",
        new_callable=AsyncMock,
        return_value=mock_resp,
    ):
        outputs = await _collect_outputs(block, input_data, TEST_CREDENTIALS)

    assert outputs["result"] == "test data"
```

**2. Run the block's inline `test_input` / `test_output` / `test_mock`** via the framework helper. The helper takes a block instance only and reads the test data defined on the block itself:

```python title="backend/blocks/my_provider/my_block_test.py"
import pytest

from backend.blocks.my_provider.my_block import MyBlock
from backend.util.test import execute_block_test


@pytest.mark.asyncio
async def test_my_block_inline_fixtures():
    await execute_block_test(MyBlock())
```

### Key Points

- **`TEST_CREDENTIALS_INPUT`** (from `_auth.py`) is used to construct the `Input` object — it contains the credential metadata (provider, id, type).
- **`TEST_CREDENTIALS`** (from `_auth.py`) is passed to `block.run(..., credentials=...)` — it contains the actual mock API key/token.
- **`execute_block_test(block)`** runs the block against its own inline `test_input` / `test_output` / `test_mock` attributes (set in `__init__`). It does **not** accept `input_data=` or `credentials=` and does **not** return outputs.
- The block attribute for test credentials is `test_credentials` (not `test_credentials_input`) when using the inline test pattern in `__init__`.
- Use `pytest.mark.asyncio` for async block `run()` methods. Mock the HTTP layer where it is *used* — typically `backend.util.request.Requests` (or `httpx.AsyncClient`), not stdlib `requests`.

### Running Tests

```bash
# Run a specific block's standalone tests
poetry run pytest backend/blocks/my_provider/my_block_test.py -xvs

# Run all block tests (inline test_input/test_output validation)
poetry run pytest backend/blocks/test/test_block.py -xvs

# Test a specific block by name (inline tests)
poetry run pytest 'backend/blocks/test/test_block.py::test_available_blocks[MyBlock]' -xvs
```

## Block Documentation Sync

After creating or modifying blocks, you must regenerate the block documentation to keep it in sync with the code.

### Generating Documentation

```bash
cd autogpt_platform/backend
poetry run python scripts/generate_block_docs.py
```

For provider packages, the generator writes one page per Python module:

```
docs/integrations/block-integrations/{provider}/{module}.md
```

For example, `backend/blocks/zerobounce/validate_emails.py` generates `docs/integrations/block-integrations/zerobounce/validate_emails.md`. Top-level block modules are grouped into category pages such as `docs/integrations/block-integrations/basic.md`.

### CI Enforcement

The **"Block Documentation Sync Check"** CI workflow (`.github/workflows/docs-block-sync.yml`) runs on every PR that touches files in `backend/blocks/` or `docs/integrations/`. It runs the generator in `--check` mode and fails the build if docs are out of sync.

If CI fails with a docs sync error, run the generator locally, commit the updated docs, and push.

### Manual Sections

The generated docs support manually-written content that is preserved across regeneration. Use these markers in the generated markdown files:

```markdown

Your custom usage examples here. This will not be overwritten.

```

Use the generator-supported block sections `how_it_works`, `use_case`, and `extras`, or file-level `file_description` and `additional_content`. Keep the existing markers rather than inventing section names. These sections hold usage examples, caveats, and integration notes that cannot be generated from block metadata.

---

### Platform Ollama (`docs/platform/ollama.md`)

# Running Ollama with AutoGPT

> **Important**: Ollama integration is only available when self-hosting the AutoGPT platform. It cannot be used with the cloud-hosted version.

Follow these steps to set up and run Ollama with the AutoGPT platform.

## Prerequisites

1. Make sure you have gone through and completed the [AutoGPT Setup](getting-started.md) steps, if not please do so before continuing with this guide.
2. Before starting, ensure you have [Ollama installed](https://ollama.com/download) on your machine.

## Setup Steps

### 1. Launch Ollama

To properly set up Ollama for network access, choose one of these methods:

**Method A: Using Ollama Desktop App (Recommended)**

1. Open the Ollama desktop application
2. Go to **Settings** and toggle **"Expose Ollama to the network"**
   
3. Click on the model name field in the "New Chat" window
4. Search for "llama3.2" (or your preferred model)
   
5. Click on it to start the download and load the model to be used

??? note "Method B: Using Docker (Alternative)"

    If you prefer to run Ollama via Docker instead of the desktop app, you can use the official [Ollama Docker image](https://hub.docker.com/r/ollama/ollama):

    1. **Start Ollama container** (choose based on your hardware):

       **CPU only:**
       ```bash
       docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama
       ```

       **With NVIDIA GPU** (requires [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)):
       ```bash
       docker run -d --gpus=all -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama
       ```

       **With AMD GPU:**
       ```bash
       docker run -d --device /dev/kfd --device /dev/dri -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama:rocm
       ```

       **Download your desired model:**
       ```bash
       docker exec -it ollama ollama run llama3.2
       ```

    !!! note
        The Docker method automatically exposes Ollama on `0.0.0.0:11434`, making it accessible to AutoGPT. More models can be found on the [Ollama library](https://ollama.com/library).

??? warning "Method C: Using Ollama Via Command Line (Legacy)"

    For users still using the traditional CLI approach or older Ollama installations:

    1. **Set the host environment variable:**

       **Windows (Command Prompt):**
       ```cmd
       set OLLAMA_HOST=0.0.0.0:11434
       ```
       
       **Linux/macOS (Terminal):**
       ```bash
       export OLLAMA_HOST=0.0.0.0:11434
       ```

    2. **Start the Ollama server:**
       ```bash
       ollama serve
       ```

    3. **Open a new terminal/command window** and download your desired model:
       ```bash
       ollama pull llama3.2
       ```

    !!! note
        This will download the [llama3.2](https://ollama.com/library/llama3.2) model. Keep the terminal with `ollama serve` running in the background throughout your session.

### 2. Start the AutoGPT Platform

Navigate to the autogpt_platform directory and start all services:

```bash
cd autogpt_platform
docker compose up -d --build
```

This command starts both the backend and frontend services. Once running, visit [http://localhost:3000](http://localhost:3000) to access the platform. After registering/logging in, navigate to the build page at [http://localhost:3000/build](http://localhost:3000/build).

### 3. Using Ollama with AutoGPT

Now that both Ollama and the AutoGPT platform are running, we can use Ollama with AutoGPT:

1. Add an AI Text Generator block to your workspace (it can work with any AI LLM block but for this example will be using the AI Text Generator block):
   

2. **Configure the API Key field**: Enter any value (e.g., "dummy" or "not-needed") since Ollama doesn't require authentication.

3. In the "LLM Model" dropdown, select "llama3.2" (This is the model we downloaded earlier)
   

   > **Compatible Models**: The following Ollama models are available in AutoGPT by default:
   > - `llama3.2` (Recommended for most use cases)
   > - `llama3.3`
   > - `llama3` 
   > - `llama3.1:405b`
   > - `dolphin-mistral:latest`
   > 
   > **Note**: To use other models, follow [Add Custom Models (Advanced)](#add-custom-models-advanced) below.

4. **Set your local IP address** in the "Ollama Host" field:

   **To find your local IP address:**

   **Windows (Command Prompt):**
   ```cmd
   ipconfig
   ```

   **Linux/macOS (Terminal):**
   ```bash
   ip addr show
   ```
   or
   ```bash
   ifconfig
   ```

   Look for your IPv4 address (e.g., `192.168.0.39`), then enter it with port `11434` in the "Ollama Host" field:
   ```
   192.168.0.39:11434
   ```

   

   > **Important**: Since AutoGPT runs in Docker containers, you must use your host machine's IP address instead of `localhost` or `127.0.0.1`. Docker containers cannot reach `localhost` on the host machine.

5. Add prompts to your AI block, save the graph, and run it:
   

That's it! You've successfully setup the AutoGPT platform and made a LLM call to Ollama.


### Using Ollama on a Remote Server with AutoGPT

For running Ollama on a remote server, simply make sure the Ollama server is running and is accessible from other devices on your network/remotely through the port 11434. 

**To find your local IP address of the system running Ollama:**

**Windows (Command Prompt):**
```cmd
ipconfig
```

**Linux/macOS (Terminal):**
```bash
ip addr show
```
or
```bash
ifconfig
```

Look for your IPv4 address (e.g., `192.168.0.39`).

Then you can use the same steps above but you need to add the Ollama server's IP address to the "Ollama Host" field in the block settings like so:
```
192.168.0.39:11434
```



## Add Custom Models (Advanced)

Model definitions are centralized in the LLM catalog — see [Managing LLM Models](contributing/managing-llm-models.md) for the full field reference and workflow. To add a custom Ollama model:

1. **Add a catalog entry** in `autogpt_platform/backend/backend/data/llm_registry/catalog.py`, next to the other Ollama models:
   ```python
   CatalogModel(
       slug="The-model-name-from-ollama",  # bare name, exactly as Ollama serves it
       display_name="Your Model",
       provider="ollama",
       context_window=8192,  # adjust for your model
       cost=CatalogModelCost(run_credits=1),
   ),
   ```
   Ollama models use bare model names (no `vendor/` prefix) — the name must match what `ollama list` shows. A run cost of `1` is fine for local usage; cost tracking is disabled for self-hosted instances.

2. **Also add one `LLMModel` name line** in `autogpt_platform/backend/backend/data/llm_registry/llm_models.py` (Ollama section) — that identifier is what block schemas serialize; every model FACT (metadata, costs) lives in the catalog entry itself, and an import-time check refuses to boot if an enum name has no catalog entry. The catalog entry is what centralizes the model's metadata and makes it routable for AutoPilot.

3. **Rebuild the backend**:
   ```bash
   docker compose up -d --build
   ```

   The prebuilt single-container image cannot pick up a new catalog entry; build it from source as described in [Run AutoGPT in One Docker Container](single-container.md#get-an-image).

4. **Pull the model in Ollama**:
   ```bash
   ollama pull your-model-name
   ```

## Troubleshooting

If you encounter any issues, verify that:

- Ollama is properly installed and running with `ollama serve`
- Docker is running before starting the platform
- If running Ollama outside Docker, ensure it's set to `0.0.0.0:11434` for network access

### Common Issues

#### Connection Refused / Cannot Connect to Ollama
- **Most common cause**: Using `localhost` or `127.0.0.1` in the Ollama Host field
- **Solution**: Use your host machine's IP address (e.g., `192.168.0.39:11434`)
- **Why**: AutoGPT runs in Docker containers and cannot reach `localhost` on the host
- **Find your IP**: Use `ipconfig` (Windows) or `ifconfig` (Linux/macOS)
- **Test Ollama is running**: `curl http://localhost:11434/api/tags` should work from your host machine

#### Model Not Found
- Pull the model manually:
  ```bash
  ollama pull llama3.2
  ```
- If using a custom model, ensure it's registered in the LLM catalog (`autogpt_platform/backend/backend/data/llm_registry/catalog.py`) and `autogpt_platform/backend/backend/data/llm_registry/llm_models.py` as described in [Add Custom Models (Advanced)](#add-custom-models-advanced) above

#### Docker Issues
- Ensure Docker daemon is running:
  ```bash
  docker ps
  ```
- Try rebuilding:
  ```bash
  docker compose up -d --build
  ```

#### API Key Errors
- Remember that Ollama doesn't require authentication - any value works for the API key field

#### Model Selection Issues
- Look for models with "ollama" in their description in the dropdown
- Only the models listed in the "Compatible Models" section are guaranteed to work

---

### Platform Scheduling And Triggers (`docs/platform/scheduling-and-triggers.md`)

# Scheduling & Triggers

## Overview

AutoGPT agents can be run on demand, on a recurring schedule, or automatically in response to external events via webhooks. This guide covers both scheduling and trigger-based execution.

## Scheduling an Agent

Scheduling lets you run an agent automatically on a recurring basis with pre-configured inputs.

### Setting Up a Schedule

1. Go to your [Agent Library](agent-library.md) and open the agent you want to schedule
2. Click **New agent task** (the same button used for manual runs)
3. Fill in all the input fields with the values you want the agent to use
4. At the bottom of the input form, you'll see two buttons: **Start Task** and **Schedule Task**
5. Click **Schedule Task**

### Configuring the Schedule

The schedule configuration screen allows you to set:

| Setting | Description |
|---------|-------------|
| **Schedule Name** | A descriptive name for this schedule |
| **Repeats** | Frequency — e.g., Weekly |
| **Repeats On** | Select specific days (individual days, weekdays, weekends, or select all) |
| **At** | The time of day to run (hour and minute) |
| **Timezone** | Schedule runs in your local timezone, displayed on screen |

**Example:** Run "Keyword SEO Expert" every weekday at 9:00 AM in the timezone shown in the schedule form.

### Managing Schedules

After creating a schedule, you can view and manage it on your agent's detail page:

- Open the agent from your library
- Select the **Scheduled** tab on the left-hand side
- The tab shows the count of active schedules (e.g., `Scheduled 1`)

## Triggers (Webhook-Based Execution)

Triggers allow external services or your own code to start an agent automatically by sending data to a webhook URL.

### How Triggers Work

Unlike standard input blocks, **trigger blocks** are a special type of input block. When you add a trigger block to your agent in the builder, the agent's execution model changes — instead of being started manually, it waits for incoming webhook events.

### Setting Up a Trigger

#### Step 1: Add a Trigger Block in the Builder

1. Open the [Agent Builder](agent-builder-guide.md)
2. From the blocks menu, add a **trigger block** to your agent
3. Connect it to the rest of your workflow like any other input block
4. Save your agent

#### Step 2: Configure the Trigger in Your Library

1. Go to your [Agent Library](agent-library.md) and open the agent
2. Click **New agent task**
3. Enter the trigger name and description and complete any required inputs or credentials
4. Click **Set up Trigger**

#### Step 3: Copy the Webhook URL

Once the trigger is created, you'll see a status panel:

> **Trigger Status**
>
> Status: **Active**
>
> This trigger is ready to be used. Use the Webhook URL below to set up the trigger connection with the service of your choosing.
>
> **Webhook URL:** `https://backend.agpt.co/api/integrations/generic_webhook/webhooks/<webhook-id>/ingress`

Copy this webhook URL and provide it to the external platform or code that will be sending events to trigger your agent.

{% hint style="info" %}
Trigger-based agents cannot be started manually. Configure them through **New agent task** and **Set up Trigger**, then send data to the webhook URL to execute them.
{% endhint %}

### Example Use Cases

- **GitHub webhook**: Trigger an agent whenever a pull request is opened
- **Payment processor**: Run an agent when a new payment is received
- **Form submission**: Process data when a user submits a form on your website
- **Custom integration**: Send data from any service that supports webhooks

## Schedule vs. Trigger

| | Schedule | Trigger |
|---|----------|---------|
| **How it starts** | Automatically at configured times | When an external event sends data to the webhook URL |
| **Input source** | Pre-configured when the schedule is created | Provided by the incoming webhook payload |
| **Use case** | Recurring tasks with fixed inputs (daily reports, weekly summaries) | Event-driven tasks (new PR opened, form submitted, payment received) |
| **Setup** | Through the **New agent task** → **Schedule Task** flow | Through trigger blocks in the builder, then **New agent task** → **Set up Trigger** in the library |

---

### Platform Sharing And Exporting (`docs/platform/sharing-and-exporting.md`)

# Sharing & Exporting Agents

## Overview

There are several ways to share agents and their outputs with others on the AutoGPT Platform. This guide covers all the available options.

## Sharing Options

### Share Task Output via URL

A completed task remains private until you explicitly enable sharing:

1. Open the agent and select the completed task.
2. Click **Share results**.
3. Review the public-access warning and click **Enable Sharing**.
4. Copy the generated **Share URL**.

Anyone with that URL can view the run's outputs. Use **Stop Sharing** in the same dialog to revoke the link.

{% hint style="warning" %}
A share URL is publicly accessible to anyone who has it. It does not publish the task inputs, but outputs may contain sensitive data.
{% endhint %}

### Publish to the Marketplace

The most visible way to share an agent is to publish it to the [Marketplace](marketplace.md). Published agents are discoverable by all platform users and go through a review process by the AutoGPT team.

See [Publishing an Agent](marketplace.md#publishing-an-agent) for the full guide.

### Export as a File

For sharing agents privately — without publishing to the marketplace — you can export an agent as a file:

1. Go to your [Agent Library](agent-library.md) and open the agent
2. Click the **three dots** (⋯) on the far right
3. Select **Export Agent to File**
4. The agent file will download to your computer

You can then send this file to anyone via email, messaging, or any other method.

### Import a Shared File

To import an agent file someone has shared with you:

1. Go to your [Agent Library](agent-library.md)
2. Click **Import**
3. Select the **AutoGPT agent** tab
4. Upload the exported `.json` file
5. The agent will be added to your library

## Teams & Collaboration

Organization and team workspaces are available only when enabled for your account or deployment. Use their built-in access controls where available. Export/import remains the portable way to transfer an agent file between separate accounts or deployments.

---

### Platform Single Container (`docs/platform/single-container.md`)

# Run AutoGPT in One Docker Container

The image packages the AutoGPT Platform frontend and backend together with
PostgreSQL, a three-node Valkey cluster, RabbitMQ, nginx, and FalkorDB.
Only nginx listens on the container's public interface, on port `3000`.
The FalkorDB service is a core part of this image and always runs; Graphiti
memory is enabled by the image's default configuration and still requires a
working model-provider profile.

All durable state is stored under `/data`. Use a named Docker volume for every
installation you intend to keep.

## Requirements

- Docker Engine or Docker Desktop capable of running Linux containers on
  `linux/amd64` or `linux/arm64`. Building locally also requires Docker Buildx.
- More than the measured 5–6 GiB of appliance memory use, with additional
  headroom for agents, local models, and workload growth.
- Host storage with enough capacity and throughput for the image, the growing
  `/data` volume, and at least one complete compressed backup archive alongside
  the live data. Shutdown and backup time depend on storage throughput and fsync
  latency; SSD-class storage is recommended but not a measured support floor.

There is no measured minimum CPU count or supported concurrent-run ceiling;
performance depends on the enabled services and workload.

## Get an image

Use `latest` for the most recent fully verified stable AutoGPT Platform image:

```bash
IMAGE=significantgravitas/autogpt:latest
docker pull "${IMAGE}"
```

For a reproducible installation, replace `latest` with an immutable `vX.Y.Z`
tag or manifest digest. Docker image tags map to source releases as follows:

- `latest` points to the newest stable AutoGPT Platform release.
- `vX.Y.Z` is the immutable image for GitHub release
  `autogpt-platform-beta-vX.Y.Z`.
- `sha-<git-sha>` is the immutable image for the exact source revision of a
  release. Each release publishes one for the commit it was built from; `dev`
  commits are built and tested in CI but not published.

`canary-sha-*` tags are unsupported pre-release validation artifacts.

To build from source instead, run Docker Buildx Bake from the repository root:

```bash
docker buildx bake \
  --file autogpt_platform/single-container/docker-bake.hcl \
  --load \
  single-container
IMAGE=autogpt-platform:single-container-dev
```

Commands below use the shell variable `IMAGE`, whether it identifies the
Docker Hub image, a pinned digest, or the local build.

The image has a complete default command, so you can optionally perform a
foreground boot check:

```bash
docker run --rm "${IMAGE}"
```

Skip this check if you want to proceed directly to a persistent installation;
Quick start performs the same first-boot work against its named volume. The
check runs in the foreground. First boot may take several minutes and use
roughly 5–6 GiB of memory; press Ctrl-C to stop it. That interruption is safe
for this check because `--rm` discards its anonymous `/data` volume. Do not
generalize it to an installation that uses a named volume. The command does not
publish the web port. Use the full setup below for a usable installation.

## Quick start

Create a private environment file:

```bash
umask 077
touch autogpt.env
chmod 600 autogpt.env
```

When working from a source checkout, you can copy
`autogpt_platform/single-container/.env.example` instead to see common optional
settings. The backend-only `BEHAVE_AS` control and its limits are documented in
[Security boundary](#security-boundary).

Edit the file and set at least the public URL and exact address for the first
account:

```dotenv
AUTOGPT_PUBLIC_URL=http://localhost:3000
AUTH_SIGNUP_ALLOWLIST=owner@example.com
```

Replace `owner@example.com` with the email address for the intended first
account. Passwords must contain at least 12 characters. Exact-address
allowlisting compares the email string asserted during signup; because this
image does not support required email verification, it is not proof of mailbox
ownership. New-account creation defaults open when the allowlist is empty; the
value above immediately restricts signup to that address. If the allowlist is
omitted, anyone who can reach the published port can create an account until
`AUTH_ALLOW_NEW_ACCOUNTS=false` is applied. The run command below binds the app
only to loopback. Create and promote the administrator and close signup while
the app is still loopback-only. Configure an HTTPS origin before entering
credentials on any LAN or remote deployment.

Provider keys are not required to boot, create an account, use Builder, or run
provider-free blocks. Model-backed functions return their normal actionable
missing-credential error until you configure a profile from
[Models and memory](#models-and-memory).

Start the appliance:

```bash
docker run --detach --name autogpt \
  --restart unless-stopped \
  --shm-size 2g \
  --ulimit nofile=65536:65536 \
  --log-driver json-file \
  --log-opt max-size=50m \
  --log-opt max-file=5 \
  --env-file autogpt.env \
  --publish 127.0.0.1:3000:3000 \
  --volume autogpt-data:/data \
  "${IMAGE}"
```

The `--shm-size 2g` allocation raises the container's `/dev/shm` above Docker's
64 MB default; the bundled PostgreSQL and browser tooling both use shared
memory. The `nofile` limit sets a predictable per-process file-descriptor
ceiling for the bundled services. The JSON log options retain about five 50 MB
files instead of allowing container logs to grow without a bound.

Wait for the complete appliance to become healthy:

```bash
docker inspect --format '{{.State.Health.Status}}' autogpt
docker logs --tail 100 --follow autogpt
```

Do not stop the container while the first boot is applying database migrations.
If a migration is interrupted, later boots refuse to continue and the logs
identify the migration and recovery choices. For a brand-new empty installation,
remove that installation's unused `/data` volume and start again. For an existing
installation, restore the pre-upgrade backup. Use `prisma migrate resolve` only
after determining whether that migration's changes reached the database.

For an empty first boot that used the exact Quick start names, first verify that
the volume contains no user data, then discard that failed installation:

```bash
docker stop autogpt
docker rm autogpt
docker volume rm autogpt-data
```

Repeat Quick start afterward. The failed appliance can remain in a restart loop
until explicitly stopped, so `docker exec` is not a reliable way to run
`prisma migrate resolve`; that advanced path requires PostgreSQL running against
the affected volume from a separate recovery environment. Do not use it without
inspecting whether the migration's database changes completed.

Test installations used about 5–6 GiB of memory during startup and steady-state
health checks. This is measured guidance, not a guaranteed minimum; allow
headroom for enabled services, agents, local models, and workload growth. On
Docker Desktop for macOS or Linux, or Windows in Hyper-V mode, make sure the
VM's memory allocation in **Settings → Resources** exceeds that observed use and
leaves the same headroom. With the default WSL 2 backend on Windows, set the
limit with `memory=` under `[wsl2]` in `%UserProfile%\.wslconfig` instead, then
run `wsl --shutdown`.

Open `http://localhost:3000`, create the intended account, and promote it:

```bash
docker exec autogpt autogpt-admin promote owner@example.com
```

Replace `owner@example.com` with the email address of the account you created.
Sign out and back in after promotion so the new session has the administrator
role.

Then set this in the environment file:

```dotenv
AUTH_ALLOW_NEW_ACCOUNTS=false
```

Apply the change by replacing only the container. Keep the same named volume:

```bash
docker stop autogpt
docker rm autogpt
```

Repeat the `docker run` command above. Removing the container does not remove
the `autogpt-data` volume.

## Stopping

`docker stop autogpt` is designed and tested to complete inside Docker's stock
10-second timeout, so no host-wide timeout change is needed; a longer host
timeout does not extend the internal Supervisor caps. The shipped one-second
runtime, five-second state, and one-second event-listener phases measured about
8.4 seconds in the shutdown test setup. That is a narrow measured margin, not a
graceful-shutdown guarantee for slower storage, so do not shorten Docker's
timeout. Runtime processes are signaled first. PostgreSQL, RabbitMQ, Valkey,
and FalkorDB are signaled afterward and each gets at most five seconds to exit
before Supervisor forces it down. The event listener stops last. Larger or
slower state may require normal crash recovery on the next boot.

Agent runs still executing when the container stops are abandoned. Their queue
messages can be dropped and their execution rows can remain `RUNNING`, but they
do not resume. Start a new run after restart.

Supervisor process names are group-qualified. Pass the appliance's Supervisor
configuration to see names such as `runtime:rest` and `state:postgres`:

```bash
docker exec autogpt supervisorctl \
  -c /opt/autogpt/single-container/supervisor/supervisord.conf status
```

`runtime:bootstrap` runs once per boot, so it shows `EXITED` after a successful
start.

## Port and public URL

Container port `3000` does not change. To use host port `3300`, change the run
command to:

```text
--publish 127.0.0.1:3300:3000
```

and set:

```dotenv
AUTOGPT_PUBLIC_URL=http://localhost:3300
```

`AUTOGPT_PUBLIC_URL` must be the exact origin used in the browser, including
the scheme and any non-default port. Docker cannot discover the host-side port
mapping from inside the container. A mismatch breaks authentication actions,
callbacks, cookies, and generated links.

For LAN or remote access, keep AutoGPT bound to loopback and place it behind a
TLS reverse proxy running outside the AutoGPT container. The proxy provides
HTTPS and forwards requests to port `3000`; AutoGPT handles application routing
internally.

Before exposing the proxy, complete the [Account policy](#account-policy)
bootstrap, promote the intended administrator, and close new-account signup.
The bundled nginx records the external proxy as the immediate client, so the
proxy must preserve its own client-attribution logs and enforce any per-client
IP rate limits or access rules.

Set `AUTOGPT_PUBLIC_URL` to the browser-visible HTTPS origin, for example:

```dotenv
AUTOGPT_PUBLIC_URL=https://agents.example.com
```

Do not expose the container's plaintext port directly or leave the URL at the
localhost default.

## Account policy

New-account creation defaults open so the first administrator can sign up. A
nonempty allowlist immediately restricts who can create an account. Existing
accounts can still sign in after signup is closed.

To allow only selected accounts during provisioning, keep signup enabled and
set an allowlist:

```dotenv
AUTH_ALLOW_NEW_ACCOUNTS=true
AUTH_SIGNUP_ALLOWLIST=owner@example.com
```

The allowlist accepts exact email addresses and entries beginning with `@` for
an entire domain. Separate multiple entries with commas, for example
`AUTH_SIGNUP_ALLOWLIST=owner@example.com,teammate@example.com`. It applies to
email/password signup and any first-time account creation through a configured
provider endpoint; the bundled UI does not show social-login buttons. Prefer
exact addresses; use a domain entry only for a domain you fully control, then
narrow the list after bootstrap. Domain matching trusts the identity provider's
asserted email; public email domains such as `@gmail.com` are not safe allowlist
entries.

Setting `AUTH_ALLOW_NEW_ACCOUNTS=false` blocks all new accounts regardless of
the allowlist; recreate the container with the same volume to apply the setting
after promoting the intended administrator.

Fresh installations should keep `AUTOGPT_ENABLE_LEGACY_AUTH=false`. Enable it
only when intentionally migrating an existing legacy symmetric-JWT setup.

Required email verification is not supported by this image and intentionally
stops startup if enabled. Keep:

```dotenv
AUTH_REQUIRE_EMAIL_VERIFICATION=false
```

Postmark can provide password-reset and email-change messages:

```dotenv
POSTMARK_SERVER_API_TOKEN=
POSTMARK_SENDER_EMAIL=
POSTMARK_WEBHOOK_TOKEN=
```

Set `POSTMARK_SENDER_EMAIL` to a sender verified by your Postmark account. This
does not add account-verification support. Without these Postmark settings,
there is no self-service password-reset email or appliance CLI for resetting a
password. Store the administrator password securely before closing signup.

### Add or recover an administrator

To add an account after signup is closed, temporarily bind the appliance to
loopback, set `AUTH_ALLOW_NEW_ACCOUNTS=true`, and set
`AUTH_SIGNUP_ALLOWLIST` to that account's exact email address. Replace the
container with the same named volume and original launch options, create the
account, and promote it:

```bash
docker exec autogpt autogpt-admin promote new-owner@example.com
```

Then set `AUTH_ALLOW_NEW_ACCOUNTS=false` and replace the container again. If an
administrator password is lost and Postmark password reset was not configured,
this procedure creates a replacement administrator; it does not reset the
existing account's password. Keep the port loopback-only throughout recovery,
or enforce equivalent HTTPS and network access controls before reopening
signup.

Setting both `AUTH_*_CLIENT_ID` and `AUTH_*_CLIENT_SECRET` values for a social
provider registers reachable OAuth sign-in and callback endpoints. The bundled
local-mode frontend does not render buttons for them, so leave each pair empty
unless you intentionally use that direct provider flow. `AUTH_SIGNUP_ALLOWLIST`
and `AUTH_ALLOW_NEW_ACCOUNTS` still gate first-time account creation through
those endpoints. Agent block OAuth integrations use the separate unprefixed
credentials. The prebuilt frontend does not support configuring Google Picker
public keys at runtime.

## Models and memory

The FalkorDB service always runs and persists under `/data`; it has no supported
process toggle in this distribution. Graphiti memory is enabled by the image's
default feature configuration.

The image does not include model-provider credentials. This does not block
startup, authentication, Builder, or provider-free blocks. Configure one of
the following profiles before expecting AutoPilot and memory extraction to
work; until then, provider-backed requests return the same missing-credential
errors as other AutoGPT deployment modes.

### OpenRouter with OpenAI embeddings

The default remote profile uses OpenRouter for chat and memory extraction and
OpenAI for embeddings:

```dotenv
CHAT_USE_LOCAL=false
CHAT_USE_OPENROUTER=true
OPEN_ROUTER_API_KEY=YOUR_OPENROUTER_KEY
OPENAI_API_KEY=YOUR_OPENAI_KEY
```

Both keys are needed for the complete memory path. `OPENAI_API_KEY` alone does
not select direct OpenAI routing for AutoPilot.

### Anthropic chat with remote memory

To route AutoPilot directly to Anthropic:

```dotenv
CHAT_USE_LOCAL=false
CHAT_USE_OPENROUTER=false
ANTHROPIC_API_KEY=YOUR_ANTHROPIC_KEY
OPEN_ROUTER_API_KEY=YOUR_OPENROUTER_KEY
OPENAI_API_KEY=YOUR_OPENAI_KEY
```

The Anthropic key changes the AutoPilot chat transport. The OpenRouter and
OpenAI keys are still required by Graphiti's default remote extraction and
embedding clients.

### Ollama or another local OpenAI-compatible server

For the default local profile, install both the chat model and memory embedding
model on the Docker host:

```bash
ollama pull hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M
ollama pull nomic-embed-text
```

Configure the host's Ollama service with `OLLAMA_CONTEXT_LENGTH=32768` and
restart Ollama before using AutoPilot. This is an Ollama-server setting, not an
`autogpt.env` entry; Ollama's smaller default context cannot hold AutoPilot's
roughly 8k-token system prompt.

The chat model and exact `Q4_K_M` artifact are published in the
[Unsloth Qwen3.5-4B-GGUF repository](https://huggingface.co/unsloth/Qwen3.5-4B-GGUF/tree/main).
Keep the model identifier in the pull command and environment setting
identical. The 4B model is the smaller default chosen for this all-in-one
appliance's shared memory budget; larger models remain optional when the host
has sufficient resources.

Then set:

```dotenv
CHAT_USE_LOCAL=true
CHAT_BASE_URL=http://host.docker.internal:11434/v1
CHAT_API_KEY=ollama
CHAT_FAST_STANDARD_MODEL=hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M
GRAPHITI_LLM_MODEL=hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M
GRAPHITI_RERANKER_MODEL=hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M
```

`CHAT_API_KEY` must be non-empty even if the local server ignores it. The
local transport points Graphiti at the same base URL, and its extraction client
uses `CHAT_API_KEY`. Graphiti's own local default is
`hf.co/ornith-ai/Ornith-1.5-9B-GGUF:Q4_K_M`, so the two `GRAPHITI_*` lines keep
extraction and reranking on the 4B chat model; embeddings use
`nomic-embed-text`. If you choose another chat model, set `GRAPHITI_LLM_MODEL`
and `GRAPHITI_RERANKER_MODEL` to a model that the endpoint serves. Set
`GRAPHITI_EMBEDDER_MODEL` too when its embedding model uses another slug.

Graphiti's embedder prefers `CHAT_OPENAI_API_KEY` or `OPENAI_API_KEY` when
either is set, and sends that key to this endpoint. If you keep
`OPENAI_API_KEY` for blocks or transcription, also set
`GRAPHITI_EMBEDDER_API_KEY` to the value of `CHAT_API_KEY`.

On Docker Engine, add `--add-host host.docker.internal:host-gateway` to the run
command for this local-model profile. Ollama must then also listen on an
address the container can reach, because it binds only to `127.0.0.1` by
default: set `OLLAMA_HOST=0.0.0.0:11434` in the same service configuration as
`OLLAMA_CONTEXT_LENGTH`, and limit port `11434` with the host firewall to the
Docker bridge and other hosts you trust. Docker Desktop provides that hostname
and reaches the host's loopback without either change. To apply the flag to an
existing container, stop and remove the container, then repeat the Quick start
command with the same named volume and the added flag. Small quantized models
reduce memory requirements, but latency and answer quality remain hardware-,
model-, and workload-dependent; select another compatible model when the
default does not meet your needs.

Check connectivity from the running appliance:

```bash
docker exec autogpt \
  curl --fail --show-error http://host.docker.internal:11434/api/tags
```

The same settings can point at a remote vLLM, LocalAI, LM Studio, LiteLLM, or
other OpenAI-compatible HTTPS endpoint, provided it serves every configured
chat and Graphiti model slug. Do not expose an unauthenticated model server to
the internet. See the
[AutoPilot local-LLM guide](copilot-local-llm.md)
for model and context-window guidance.

Additional provider keys consumed by backend blocks may be placed in the same
environment file. Most remain backend-only. The Next.js server process always
receives the required `BETTER_AUTH_SECRET` and can receive configured `AUTH_*`
provider client secrets, `OPENAI_API_KEY`, `TRANSCRIPTION_API_KEY`, and the
legacy `SUPABASE_JWT_SECRET`. These values remain server-side process
environment and are not baked into the browser bundle.

### AutoPilot spend caps

AutoGPT Cloud limits each account's AutoPilot spend per day and per week,
measured from the provider-reported cost of every chat turn. This image
disables both caps by default because you pay the model provider directly:
`CHAT_DAILY_COST_LIMIT_MICRODOLLARS` and `CHAT_WEEKLY_COST_LIMIT_MICRODOLLARS`
are set to `-1` unless you provide a value. To guard an OpenRouter or
Anthropic bill, set either variable to a positive amount in microdollars
(1 USD = `1000000`):

```dotenv
CHAT_DAILY_COST_LIMIT_MICRODOLLARS=5000000
CHAT_WEEKLY_COST_LIMIT_MICRODOLLARS=20000000
```

The daily window resets at midnight UTC and the weekly window at Monday
00:00 UTC. Once a window is exhausted, further AutoPilot turns are refused
with HTTP 429 until it resets. The caps only meter the platform-routed chat
transport: ChatGPT/Codex subscription turns are exempt, and local
OpenAI-compatible servers report no cost, so they never count against a cap.

### Database connection tuning

`DB_CONNECTION_LIMIT` controls each backend role's Prisma connection pool and
accepts `1` through `5` (default `5`). `DB_CONNECT_TIMEOUT` controls connection
setup and accepts `1` through `600` seconds (default `60`), while
`DB_POOL_TIMEOUT` controls how long a request can wait for a pooled connection
and accepts `1` through `3600` seconds (default `300`). All roles share the
bundled PostgreSQL instance, so do not raise per-role limits beyond the enforced
range. If requests stall under concurrent runs, inspect service and PostgreSQL
logs for pool exhaustion before changing these values.

## Security boundary

`BEHAVE_AS` defaults to `local`, which bypasses subscription entitlement gating
for policies that opt into a local exemption, which currently includes every
defined entitlement. `local` is a broader product behavior profile: it can also
enable blocks intentionally disabled in hosted mode, select self-host model
routing, and change diagnostics or telemetry behavior. This is appropriate for
single-tenant self-hosting. Any
multi-tenant or hosted deployment must set `BEHAVE_AS=cloud` for backend
entitlement enforcement, but that setting is not a complete hosted-mode switch:
the bundled frontend is compiled in local mode. Cloud mode also expects the
hosted model catalog, subscription tiers, and payment controls. Do not treat
this image as a turnkey multi-tenant hosted distribution. Use the supported
cloud deployment and build, and review its full security boundary.

The browser-facing nginx and Next.js processes run under Unix identities that
are separate from backend services. The frontend receives an explicit runtime
environment allowlist and connects to PostgreSQL through a passwordless local
peer role restricted to the Better Auth tables and columns it needs. It does
receive `BETTER_AUTH_SECRET`, configured `AUTH_*` social-login client secrets,
and optional OpenAI, transcription, and legacy-JWT values because the Next.js
server uses them. Those values remain server-side and are not baked into the
browser bundle. The frontend does not receive the PostgreSQL superuser password,
RabbitMQ or Valkey passwords, the FalkorDB password, the internal service key,
or encryption keys.

Generated database, queue, cache, memory, encryption, authentication,
signing, and internal service secrets are created on first boot and stored in
`/data/config/runtime.env` as `root:root` mode `0600`. Reusing the named volume
reuses those secrets. Supplying a different value for a persisted secret on a
later boot fails instead of silently rotating it. A volume created before the
internal service key existed gets one the first time a newer image starts.

### Suspected secret exposure

There is no supported in-place rotation for the generated secrets in
`/data/config/runtime.env`. If that file or a plaintext backup is exposed,
isolate the installation and revoke external provider and OAuth credentials.
Create a replacement installation on a new volume, transfer only non-secret
agent definitions through supported export/import flows, and reconnect
credentials. Every backup from the old installation contains the same generated
secrets, so restoring an older one does not rotate them. Do not edit
`runtime.env`, selectively mix state directories, or treat a same-installation
backup as secret-exposure recovery.

These controls limit compromise between co-located processes, but Docker daemon
administrators and anyone who can read the data volume remain fully trusted.
Treat the host environment file, `/data`, backups, and unredacted diagnostic
output as secret-bearing material.

Only port `3000` should be published. Internal AppService RPC is bound to the
container's loopback interface and requires the internal service key, and
Valkey traffic requires authentication.
On Linux, Docker-managed forwarding can bypass firewall policy expressed only
through tools such as UFW. The loopback address in
`--publish 127.0.0.1:3000:3000` is the exposure control; changing it to a
non-loopback address can expose the app regardless of an INPUT-chain rule.

## Optional processes

FalkorDB is mandatory. Bot services and their internal linking manager default
off. After configuring a supported chat adapter and its public routes, enable
both optional processes with:

```dotenv
AUTOGPT_ENABLE_BOT_SERVICES=true
```

For Discord, configure `AUTOPILOT_BOT_DISCORD_TOKEN`. Telegram requires both
`AUTOPILOT_BOT_TELEGRAM_TOKEN` and
`AUTOPILOT_BOT_TELEGRAM_WEBHOOK_SECRET`, plus a public HTTPS webhook registered
with Telegram. Leave the toggle `false` until the chosen adapter credentials
and routes are ready. This setting stops processes; it does not make the image
smaller.

## Persistence

The named volume mounted at `/data` contains all durable appliance state:

| Path | Contents |
| --- | --- |
| `/data/config` | Generated runtime secrets and backend configuration |
| `/data/postgres` | Authentication and platform data |
| `/data/rabbitmq` | Queue state |
| `/data/valkey` | Three-node Valkey state |
| `/data/falkordb` | Graphiti memory data |
| `/data/workspaces` | User workspaces |
| `/data/store-media` | Marketplace images and videos when GCS is not configured |
| `/data/home` and `/data/frontend-home` | Application home directories |
| `/data/cache` | Regenerable backend and Next.js caches (excluded from backups) |

Do not mount one volume into two running AutoGPT containers. Use a different
named volume for every installation.

To confirm which volume a container uses:

```bash
docker inspect --format \
  '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Name}}{{end}}{{end}}' \
  autogpt
```

## Cold backup

The block below stops the running appliance before archiving its coupled
service state. It uses the running container's exact local image ID captured
before the stop, writes to a unique partial file, and promotes it to the final
timestamped name only after `tar` succeeds. This produces a stopped-volume
snapshot. If a state service exceeds its five-second shutdown cap, the snapshot
reflects a crash stop rather than a fully graceful shutdown. Rehearse
restoration and verify service recovery before relying on it.
The appliance remains unavailable while `tar` creates and gzip-compresses the
archive; duration depends on `/data`, host storage, and CPU performance. Before
stopping it, ensure the host filesystem that contains `BACKUP_DIR` has room for
a complete compressed archive. The block enforces owner-only mode `0700` on
that directory, including when it already exists:

Start the appliance and verify full health before backing it up. The block
intentionally refuses an already stopped or crashed appliance because it must
own the coordinated transition from running to stopped state.

There is no supported consistent hot-backup procedure for this coupled volume.
If this outage is unacceptable, use a distributed deployment and the
service-native backup procedures for its independently managed data services.

```
/* Detailed source-code truncated for AI context efficiency. */
```

By default, the block restarts the unchanged installation as soon as the
archive completes, before calculating its checksum. The exit trap also attempts
a restart if a backup command fails. For an upgrade, export
`RESTART_AFTER_BACKUP=false` only for that one block and immediately `unset`
it afterward, whether the backup succeeds or fails. A successful upgrade backup
then leaves the appliance stopped at the cutover snapshot. With the default
setting, verify that it is running again:

```bash
docker inspect --format '{{.State.Status}}' autogpt
```

The atomic lock is a stopped helper container named `autogpt-backup-lock`, so it
serializes backups of `autogpt` even when callers choose different backup
directories. Catchable exits and signals remove it. After a shell is killed with
`SIGKILL` or the host crashes, first verify that no backup process is still
running; then remove only a stale lock with
`docker rm --volumes autogpt-backup-lock` before retrying. Inspect `BACKUP_DIR`
for stale `*.partial` files from the interrupted run and remove them only after
the same check. They can contain plaintext secrets and are not valid backups.

The block writes the checksum to `<archive>.sha256` and prints the container's
configured image reference, an immutable repository digest when available, and
the host-local image ID. Record an immutable tag or digest that resolves on the
restoring host, the environment file, and the Git commit beside the archive.
For a local-only build, preserve the image separately with an immutable registry
tag or `docker save`, and load it before restoring. The archive is plaintext and
contains user content, provider credentials, auth keys, and database passwords.
Encrypt it with an approved backup mechanism and remove unencrypted staging
copies.

The anonymous-volume guard also rejects a 64-character lowercase hexadecimal
volume name as a conservative fallback for Docker engines that omit the
anonymous-volume label. Use a conventional descriptive name for a manually
created volume.

## Restore into a new volume

Restore into a new named volume so the source remains recoverable. Set
`BACKUP_FILE` to the timestamped archive name and `RESTORE_IMAGE` to the
immutable tag or digest recorded with that backup. If the archive is not under
`./autogpt-backups`, also set `BACKUP_DIR` before running the block:

```bash
BACKUP_FILE=autogpt-data-YYYYMMDDTHHMMSSZ.tgz
RESTORE_IMAGE=significantgravitas/autogpt@sha256:RECORDED_DIGEST
# BACKUP_DIR=/absolute/path/to/autogpt-backups
```

```
/* Detailed source-code truncated for AI context efficiency. */
```

A per-run ownership label is checked after volume creation and again before
failure cleanup. If another run wins the same volume name, this restore refuses
to populate or remove that volume.

Use only an archive and checksum obtained through a trusted backup process. A
matching untrusted checksum detects accidental corruption but does not prove
who created the archive. Restore extraction has no network access, but `tar`
runs as root and preserves archive ownership and mode bits, including setuid
bits. Do not extract an archive from an untrusted source or assume network
isolation makes malicious archive content safe.

Validate the restored layout without starting application services or allowing
network access. Set `RESTORE_VOLUME` to the volume printed above and reuse the
same `RESTORE_IMAGE`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

The check also rejects PostgreSQL configuration that enables non-loopback
listening, loads external configuration fragments or libraries, redirects
`hba_file`, configures WAL archiving, or weakens the generated local `peer` and
loopback `scram-sha-256` access rules.
It does not prove that each database can start. A full
recovery rehearsal boots live schedules, stored credentials, and executors,
and some services fetch runtime data during startup. Perform it only on a
dedicated egress-filtered host or network after revoking or replacing
production provider and integration credentials. There is no generic appliance
switch that safely disables every possible outbound action. Retain both
volumes until the restore is accepted.

Never selectively mix service directories from different backups.

After accepting the restore, make sure no other container uses the `autogpt`
name or host port. Set `ENV_FILE` to the recorded environment file's absolute
host path. Set `PUBLISH_SPEC` to the original publish mapping when it was not
`127.0.0.1:3000:3000`. For a Docker Engine local-model installation, also set
`ADD_HOST_SPEC=host.docker.internal:host-gateway`; leave it empty otherwise.
Then launch the restored installation with the recorded image and volume:

```bash
ENV_FILE=/absolute/path/to/autogpt.env
RESTORE_VOLUME=autogpt-data-restored-YYYYMMDDTHHMMSSZ-PID-RANDOM
RESTORE_IMAGE=significantgravitas/autogpt@sha256:RECORDED_DIGEST
# PUBLISH_SPEC=127.0.0.1:3300:3000
# ADD_HOST_SPEC=host.docker.internal:host-gateway
```

```bash
(
  set -euo pipefail
  : "${ENV_FILE:?Set ENV_FILE to the recorded host environment-file path}"
  : "${RESTORE_VOLUME:?Set RESTORE_VOLUME to the restored volume name}"
  : "${RESTORE_IMAGE:?Set RESTORE_IMAGE to the recorded immutable image}"
  PUBLISH_SPEC="${PUBLISH_SPEC:-127.0.0.1:3000:3000}"
  ADD_HOST_SPEC="${ADD_HOST_SPEC:-}"
  NETWORK_DOCKER_ARGS=(--publish "${PUBLISH_SPEC}")
  if [[ -n "${ADD_HOST_SPEC}" ]]; then
    NETWORK_DOCKER_ARGS+=(--add-host "${ADD_HOST_SPEC}")
  fi

  [[ "${ENV_FILE}" = /* && -f "${ENV_FILE}" ]] || {
    printf 'ENV_FILE must be an existing absolute host path: %s\n' \
      "${ENV_FILE}" >&2
    exit 1
  }
  if ! docker volume inspect "${RESTORE_VOLUME}" >/dev/null 2>&1; then
    echo "Restored volume does not exist: ${RESTORE_VOLUME}" >&2
    exit 1
  fi
  if docker container inspect autogpt >/dev/null 2>&1; then
    echo "Refusing to replace an existing container named autogpt" >&2
    exit 1
  fi

  docker run --detach --name autogpt \
    --restart unless-stopped \
    --shm-size 2g \
    --ulimit nofile=65536:65536 \
    --log-driver json-file \
    --log-opt max-size=50m \
    --log-opt max-file=5 \
    "${NETWORK_DOCKER_ARGS[@]}" \
    --env-file "${ENV_FILE}" \
    --volume "${RESTORE_VOLUME}:/data" \
    "${RESTORE_IMAGE}"
)
```

## Upgrade and rollback

Before an upgrade:

1. Record the running image reference and image ID.
2. Pull or build the new image while the old appliance remains available.
3. Run `export RESTART_AFTER_BACKUP=false`, then run the Cold backup block once.
   Immediately run `unset RESTART_AFTER_BACKUP` afterward, whether the backup
   succeeds or fails. A successful backup leaves the appliance stopped; a
   failed backup restarts the unchanged installation.
4. Remove only the stopped container with `docker rm autogpt`.
5. Repeat the Quick start run command with the same environment file, named
   volume, publish mapping, `--add-host`, and other original launch options, but
   the new image reference.
6. Wait for full health, then test login, memory, one agent execution, streaming,
   WebSockets, and persistence across one restart.

Useful image evidence is available with:

```bash
docker inspect --format '{{.Config.Image}} {{.Image}}' autogpt
```

Startup applies database migrations before publishing readiness. Do not run an
older image against a volume already migrated by a newer image. Rollback means
running the prior image with its matching pre-upgrade archive restored into a
new volume.

## Health and troubleshooting

Docker health checks every bundled dependency and application role:

```bash
docker inspect --format '{{.State.Health.Status}}' autogpt
docker exec autogpt autogpt-healthcheck
docker logs --follow --tail 100 autogpt
```

`GET /healthz` checks nginx only; it is not proof that the whole appliance is
ready. Once bootstrap has applied migrations and published readiness, the
watchdog allows 600 seconds for initial full health; if that deadline expires,
it stops the container and `--restart unless-stopped` begins another startup
attempt.

After migrations, each boot also publishes the public skills catalog from
GitHub; it supplies the Skills Hub skills and the expert roster. The step is
best-effort: if GitHub is unreachable it gives up after at most 10 minutes,
logs a warning, and startup continues without the catalog. Set
`AUTOGPT_PUBLISH_SKILLS=0` to skip it, for example on an egress-filtered host.

| Symptom | What to check |
| --- | --- |
| The browser cannot connect after `docker run --rm "${IMAGE}"` | A bare run does not publish a port. Use the complete Quick start command. |
| Port `3300` opens but auth actions fail | Use `--publish 127.0.0.1:3300:3000` and set `AUTOGPT_PUBLIC_URL=http://localhost:3300`, then replace the container. |
| Signup shows **Email Not Allowed** | Inspect the API response or container logs to distinguish closed registration from an allowlist miss. Set `AUTH_ALLOW_NEW_ACCOUNTS=true` with an exact-address `AUTH_SIGNUP_ALLOWLIST=owner@example.com`, replace the container, create the intended accounts, and close signup again. |
| The container remains `starting` or becomes `unhealthy` | First boot can take several minutes. Run `autogpt-healthcheck` and inspect container logs for the first failed service. |
| The container is OOM-killed or repeatedly restarts during startup | The appliance uses about 5–6 GiB before workload headroom. On Docker Desktop, increase the VM memory allocation under **Settings → Resources**, or in `.wslconfig` with the Windows WSL 2 backend. |
| Startup refuses to continue after an interrupted migration | Follow the empty-install versus existing-install recovery procedure in [Quick start](#quick-start). The restart-looping container cannot reliably run `docker exec`; do not mark the migration applied or rolled back until you verify which database changes completed. |
| Startup rejects `DB_CONNECTION_LIMIT`, `DB_CONNECT_TIMEOUT`, or `DB_POOL_TIMEOUT` | Use an integer in the supported range: `1`–`5`, `1`–`600`, and `1`–`3600`, respectively. |
| Startup rejects legacy JWT secrets | Remove `JWT_VERIFY_KEY` and `SUPABASE_JWT_SECRET` for a fresh Better Auth installation. Set `AUTOGPT_ENABLE_LEGACY_AUTH=true` only for an intentional legacy-auth migration, and set both legacy variables to the same shared secret of at least 32 characters. |
| A run stays `RUNNING` without progress after a restart | The container stopped while the run was in flight. Its message was dropped and the row was not reconciled; start a new run. |
| Requests stall for minutes under concurrent runs | Inspect backend and PostgreSQL logs for connection-pool exhaustion. `DB_CONNECTION_LIMIT` cannot be raised above its default maximum of `5`; lowering `DB_POOL_TIMEOUT` makes pool exhaustion fail sooner but does not add capacity. Reduce concurrency or move to a distributed deployment when the fixed pools are insufficient. |
| AutoPilot returns a provider `401` | Configure the key for the selected transport. The default remote route needs `OPEN_ROUTER_API_KEY`; complete remote memory also needs `OPENAI_API_KEY`. |
| Local chat works but memory ingestion fails | Install the configured embedding model and confirm its `/v1/embeddings` endpoint works. Under the local transport Graphiti defaults to `hf.co/ornith-ai/Ornith-1.5-9B-GGUF:Q4_K_M` and `nomic-embed-text`; if the server does not serve those slugs, set and install `GRAPHITI_LLM_MODEL`, `GRAPHITI_RERANKER_MODEL`, and `GRAPHITI_EMBEDDER_MODEL` explicitly. |
| Ollama cannot be reached | Keep the host-gateway option, ensure Ollama listens on an address Docker can reach, and test `/api/tags` from inside the container. |
| The container exits after a persistent health failure | The watchdog intentionally stops the appliance. Keep `--restart unless-stopped` so Docker can recover it. |
| Data appears missing after replacement | The new container is using another or anonymous `/data` volume. Inspect its mount and reattach the original named volume. |
| Restore launch reports the name `autogpt` or the host port is already in use | Inspect the leftover container and its `/data` mount. Free the intended host port, then run `docker rm autogpt` only after confirming the restored volume and launch settings are the intended replacement. |

## Known limitations

- One container is one failure, maintenance, scaling, and security boundary.
- The bundled frontend is compiled in local mode. Setting backend
  `BEHAVE_AS=cloud` does not create a supported multi-tenant hosted deployment.
- PostgreSQL, Valkey, RabbitMQ, FalkorDB, browser tooling, and the
  application compete for the same host resources.
- Valkey and FalkorDB have no appliance-level memory ceiling or eviction policy;
  their persisted working sets can grow until host memory or disk is exhausted.
  Monitor both resources and back up before capacity changes.
- Each backend role's Prisma pool has at most five PostgreSQL connections. The
  scheduler also has two SQLAlchemy job-store pools of three connections each.
  FalkorDB is configured for at most 25 queued queries and a 1-second query
  timeout. These fixed ceilings limit concurrency in the single-container
  distribution.
- All durable services share one volume and one backup schedule.
- There is no supported in-place conversion from the appliance to the
  multi-container or hosted deployment. Provision the destination separately
  and use agent export/import where supported; accounts, run history, schedules,
  credentials, and memory are not migrated automatically.
- Uploaded files are not scanned for malware. Unlike the hosted platform, this
  image bundles no antivirus daemon, so treat every upload as trusted input.
- Required email verification is unsupported.
- The prebuilt frontend cannot configure Google Picker public keys at runtime.
- Remote TLS termination is operator-supplied.
- The local `bash_exec` fallback depends on host support for Bubblewrap user and
  network namespaces. Do not make the entire appliance privileged to work
  around a host that disables them.

---

### Platform Skill Learning (`docs/platform/skill-learning.md`)

# Nightly skill learning

AutoPilot and Experts can turn work they actually verified into reusable
skills. This page describes what the first release does, what it does not
do, and how to inspect and control it. The feature is off by default and
enabled per cohort with the `dream-skill-learning-enabled` flag. The dreaming
pipeline runs learning after its memory stages when this flag is enabled.
Learning also has an independent nightly job, so it can run without enabling
other memory stages. Both paths share the same lease and revision ledger.

## What happens

1. **During work** the chat services record a bounded reference to each
   completed user turn as a *learning source revision*: message references,
   typed tool outcomes (an exit code, a completed run, a passing validation),
   and the user's explicit, unqualified confirmations. The record includes
   the persisted user message that started the turn, so a confirmation is
   attributed to a real stored message and never to an invented reference.
   No model writes a lesson at this point, and recording never blocks a
   reply.
2. **Overnight** (03:00 in the user's timezone, with a capped catch-up after
   downtime) a per-user job reviews eligible revisions. A source is eligible
   only when it still exists, still belongs to the same owner and Expert, is
   not excluded, learning is not paused for that Expert, and it carries a
   concrete outcome signal. Plans, unanswered questions, and unsupported
   "done" claims are skipped without a model call. Idle accounts cost
   nothing: with no new eligible work the pass exits before any model call.
3. The reviewer model (routed and billed like ordinary work) looks for
   work worth keeping: repeatable multi-step workflows, non-obvious error
   recoveries, and user corrections that improve recurring work. Trivial
   successes, generic advice, chat logs, and duplicate procedures are skipped.
   It proposes one
   bounded change: update an existing skill, create a meaningfully distinct
   one, or skip. Deterministic checks then require a valid slug, the required
   sections (`Steps`, `Verification`), a concrete verification statement, and
   citations of evidence spans that carry a checked outcome. Rejected
   metadata is never persisted.
   An existing skill can be rewritten only when its complete body was
   included in the review. Skills outside the bounded body window still
   appear by name and description to prevent duplicate creation.
4. **Publication** re-checks everything live inside one database
   transaction: source eligibility, epoch and revision, the skill's own
   policy, and the head version (compare-and-swap). A concurrent human edit,
   pause, or exclusion produces a conflict or a refusal, never an overwrite.
   Creation also checks that the target file or current version has not
   appeared since review. Malformed existing files are preserved.
   The workspace `SKILL.md` is written afterwards, inside the registry's
   per-owner write lock and only while the head still names that version:
   a write resumed after a newer human correction is abandoned rather than
   placed on top of it. An interrupted write is finished (or abandoned) by
   the next run without a second model call. Reading the skill also retries
   its current pending write, even with background learning disabled;
   an owner use-pause still prevents loading it.
5. **Retrieval** pins the exact version a conversation loads. A load is
   recorded as a load, never as a success. An existing conversation sees an
   overnight update at its next user turn without starting a new chat.

Every completed write to a skill — overnight, during work, an owner edit, a restore,
or an import — appends an immutable version with its origin and the
contributing source revisions. A version restored from history inherits
the restored content's sources.

Learned skills stay private to the account and Expert that did the work.
Personal conversations teach the account's personal assistant; an Expert's
conversation teaches only that Expert. The learning process never creates
a marketplace listing or automatically shares a skill. Existing imported
skills require the owner's decision before an automated change is applied.

## Evidence and states

Evidence labels are concrete: **Worked once in the source**, **Requested by
user**, **Loaded N times · Outcome unknown**, **Reported as working by you**.
"Passed checks in N later uses" is reserved for recorded checks and is not
produced by an owner's report.

Recorded verification can be retrieved outside the newest 120 messages.
Up to 40 retained verification references and nearby context are fetched
within the same owner and source revision. Complete proof takes priority
within the 24,000-character evidence budget; missing or oversized proof is
still refused.

| State                    | Meaning                                                      |
| ------------------------ | ------------------------------------------------------------ |
| Ready to use             | Published and passed the applicable checks                   |
| Needs your decision      | A proposal for a human-controlled skill or an uncertain match |
| Blocked by content check | A credential-like value was found; only the pattern class and an ordinal step are recorded |
| Paused (learning)        | Future automated changes are off for the skill or Expert     |
| Paused (not in use)      | The owner stopped using the skill; history is kept           |
| Archived                 | Superseded, stale, or invalidated by a revoked source        |
| Deleted                  | The owner deleted the skill; its history is kept, archived   |

## Controls

Learned skills can contain a concise `SKILL.md` entrypoint together with
reusable scripts, reference material, fixtures, and templates. The reviewer
uses code and material visible in verified evidence, and records the limits
of what was checked. A simple procedure can remain a single Markdown file.
Supporting files are saved with the immutable version, so retrying a save,
applying an update, or restoring a version preserves the corresponding
package. Older versions created before file snapshots were introduced
retain their existing Markdown-only restore behavior.

The Changes and decision views show added, changed, and removed package
files with bounded text previews. The instructions editor changes the
entrypoint; supporting file changes shown in a proposal are retained when
applying an edited alternative. New scripts are not executed automatically
during dreaming. A later task loads the package into its working directory
and runs the applicable checks.

Discovery triggers may contain up to 512 characters each. The per-turn
index displays a shortened combined hint when necessary; the full phrases
remain in the saved skill. Metadata and package constraints are checked
before a version is committed. A temporary storage failure retries the saved
package without another review; a permanent validation failure is settled
instead of retried indefinitely.

- **Expert page → Skills**: a recent-change line, per-skill learning lines,
  and a detail sheet with Summary, Changes (before/after by step, with the
  raw Markdown diff on request), Sources, and History. The sheet offers
  *Automatic improvements*, *Pause learning*, *Stop using this skill*, *Edit
  skill* (applies immediately), *Report an outcome* (evidence for a later
  review), and *Restore* (a new version; future use only).
- **Settings → Memory**: a learning history filtered by scope, origin, and
  state, and one list of open decisions across Experts. Personal (AutoPilot)
  skills open the same detail sheet here, with the same controls, since they
  have no Expert page.
- **Chat**: a once-per-load "Skill loaded" chip naming the exact version and
  origin. It links to that exact skill and version: the Expert page for an
  Expert skill, Settings → Memory for a personal one. A conversation also
  offers "Exclude from learning".

Editing always starts from the current version (restore a historical one
first to build on it). The version the editor saw is submitted with the
edit and checked under the same write lock, so a change that landed while
the owner was typing — overnight or from another tab — makes the save a
conflict with the draft kept, never a silent overwrite of that change.
A human edit turns automatic improvements off for that skill unless the
editor keeps them on. An owner restore also turns them off and records a
suppression for the undone behaviour, so the same change is not reapplied
silently; a near-identical proposal becomes a labelled decision instead.
Excluding a source makes every version that depends on it unavailable,
restores the newest independent version when one exists, and otherwise
pauses the skill with an explanation. That fallback restore leaves automatic
improvements off but does not suppress the behavior: independent valid evidence
can support a new proposal for the owner to decide. A source the viewer can no longer
open shows **Evidence not visible to you** without a title or link.

Deleting a skill ends its history. Its versions stay in History, archived,
after a *Deleted* entry, and excluding one of their sources later restores
nothing. The deleted behaviour is suppressed as an undone change is, so
learning never reapplies it and a near-identical proposal becomes a decision.
A genuinely different skill can still be learned under the same name; it
starts a new history with the default controls.

## Adapters

The learner never inspects an application's schema. A source adapter
implements `revalidate(source_id, revision, scope, approval_event_id)` and
`load_evidence(...)`; approval-aware kinds also expose an approval
checkpoint (event id, actor, approved revision). Publication is conditional
on that live answer; a static approved flag in a queued payload is not
enough. The first release ships the ordinary-chat adapter only.

## Operators

`GET /api/skill-learning/status` reports the backlog, the oldest pending
source, the last review and last applied change, reviews still retrying,
and the last 30 days of review cost. Every review disposition — applied,
skipped, no novel procedure, deferred, conflict, blocked by content check,
provider error, budget exhausted, stale eligibility, inaccessible evidence,
suppressed — is kept per source revision in the review ledger.

`POST /api/skill-learning/run` runs the same bounded learning pass immediately
for the authenticated account. It preserves the normal billing, scope,
content, and ownership checks. A second run with no new evidence makes no
model calls. Runs renew their lease between sources and check it again after
each model review; losing the lease leaves the source pending without
publishing the proposed change.

Validated paid review output can be retried without another model call,
including when the current budget cannot fund new calls. Its checkpoint is
limited to 512 KiB and seven days; owner, source, approval, and target state
are checked again before applying it. Uncacheable failures and ambiguous
crashes consume at most three model-call reservations per source revision
and policy. After that limit the revision is skipped; new verified evidence
can be considered in a later revision. A reservation does not prove that
the provider completed a call.

At the saved-skill limit (150 for the account or for each Expert), learning
saves nothing that would need another saved slot: a new skill, or an edit
that would move an installed copy into the saved budget. The revision is
recorded as paused before any version exists, and its paid review is kept
for the retry above, so it is saved once there is room. If another save
takes the last slot between that check and the write, the new version waits
as a pending write and is saved, without another review, once there is
room. Improvements to skills already saved continue.

Owner-approved content-check exceptions are stored with the immutable
version as exact match hashes, file paths, and occurrence counts. Restoring
or improving a version preserves only retained approved occurrences.
New, duplicated, relocated, and seeded values still require the applicable
checks.

Every registry package write replaces the whole package in one change:
every file is stored first, then the package's files are swapped in a
single database transaction. A failed or interrupted write leaves the
previous package exactly as it was; retry it to apply the change. Versioned
pending writes stay unavailable until recovery completes. Repeating source exclusion
also resumes any interrupted head repair, while preserving an owner use-pause.

## Not in the first release

A morning digest, per-section edit protection, selective forgetting
cascades, automatic sharing between Experts, skill merges or renames,
age-based archiving, additional source adapters, and any automatic
execution of a newly written recipe.

---

### Platform Submit Agent To Marketplace (`docs/platform/submit-agent-to-marketplace.md`)

# **How to Submit an Agent to the AutoGPT Marketplace**

<center><iframe width="560" height="315" src="https://www.youtube.com/embed/o42HPN7SihU?si=lGeNFN-xIDSFqaQN" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></center>

## **Prerequisites**
* A completed agent built using the AutoGPT Builder
* Your agent must be saved before submission

## **Submission Steps**
1. **Access the Submission Form**
    * Navigate to the marketplace
    * Click the "Submit Agent" button
2. **Select Your Agent**
    * Use the dropdown menu to select your completed agent
3. **Fill Out Required Information**
    * Add a detailed description of your agent
    * Enter the author name
    * Add at least one keyword to help users find your agent
    * Select the most relevant category for your agent
4. **Review Terms**
    * Read and agree to the marketplace terms
5. **Submit for Review**
    * Click the "Submit" button to complete your submission

## **After Submission**
* Your agent will enter a "pending" state
* The AutoGPT team will review your submission
* If approved, your agent will appear in the marketplace as a new listing


## **Tips for a Successful Submission**
* Provide a clear, detailed description of what your agent does
* Choose relevant keywords that accurately describe your agent's functionality
* Ensure your agent is fully tested and working as intended before submission
* Select the most appropriate category to help users find your agent

---

### Platform Templates (`docs/platform/templates.md`)

# Templates

## Overview

Templates save a completed task's inputs and selected credential references so you can start another task with the same configuration.

## Creating a Template

Templates are created from previously completed tasks:

1. Go to your [Agent Library](agent-library.md) and open the agent
2. Find a completed task in the left-hand pane whose inputs you want to reuse
3. Click the **three dots** (⋯) on the task
4. Select **Save as Template**
5. Give the template a **name** and **description**
6. Click **Save**

The template stores the task's input values, selected credential references, graph ID, and graph version. It does not copy credential secrets.

## Using a Template

To run an agent using a saved template:

1. Open the agent from your library
2. Switch to the **Templates** tab on the left-hand side
3. Click the template you want to use
4. Review or edit its saved inputs and credential selections
5. Select **Start task from template**

## Managing Templates

Templates are listed under the **Templates** tab on your agent's detail page. The tab shows the count of available templates (e.g., `Templates 3`).

## When to Use Templates

Templates are particularly useful for:

- **Recurring tasks**: When you run an agent regularly with the same inputs (e.g., a daily SEO report for the same keyword)
- **Standard configurations**: When you've found input settings that work well and want to reuse them
- **Quick re-runs**: When you want to repeat a successful task without re-entering all the inputs

{% hint style="info" %}
A template is pinned to the agent graph version from which it was created. Later agent edits do not automatically update the template to a newer graph version.
{% endhint %}

---

### Platform What Is Autogpt Platform (`docs/platform/what-is-autogpt-platform.md`)

# What is the AutoGPT Platform?

The AutoGPT Platform is a groundbreaking system that revolutionizes AI utilization for businesses and individuals. It enables the creation, deployment, and management of continuous agents that work tirelessly on your behalf, bringing unprecedented efficiency and innovation to your workflows.

## Key Features

* **Seamless Integration and Low-Code Workflows**: Rapidly create complex workflows without extensive coding knowledge.
* **Autonomous Operation and Continuous Agents**: Deploy cloud-based assistants that run indefinitely, activating on relevant triggers.
* **Intelligent Automation and Maximum Efficiency**: Streamline workflows by automating repetitive processes.
* **Reliable Performance and Predictable Execution**: Enjoy consistent and dependable long-running processes.

## Platform Architecture

The AutoGPT Platform consists of two main components:

### 1. AutoGPT Server

The powerhouse of our platform, containing:

* **Source Code**: Core logic driving agents and automation processes.
* **Infrastructure**: Robust systems ensuring reliable and scalable performance.
* **Marketplace**: A comprehensive marketplace for pre-built agents.

### 2. AutoGPT Frontend

The user interface where you interact with the platform:

* **Agent Builder**: Design and configure your own AI agents.
* **Workflow Management**: Build, modify, and optimize automation workflows.
* **Deployment Controls**: Manage the lifecycle of your agents.
* **Ready-to-Use Agents**: Select from pre-configured agents.
* **Agent Interaction**: Run and interact with agents through a user-friendly interface.
* **Monitoring and Analytics**: Track agent performance and gain insights.

## Platform Components

### Agents and Workflows

In the platform, you can create highly customized workflows to build agents. An agent is essentially an automated workflow that you design to perform specific tasks or processes. Create customized workflows to build agents for various tasks, including:

* Data processing and analysis
* Task scheduling and management
* Communication and notification systems
* Integration between different software tools
* AI-powered decision making and content generation

### Blocks as Integrations

Blocks represent actions and are the building blocks of your workflows, including:

* Connections to external services
* Data processing tools
* AI models for various tasks
* Custom scripts or functions
* Conditional logic and decision-making components

You can learn more under: [Build your own Blocks](new_blocks.md)

## Available Language Models

The full model list — models, capabilities, context windows, and pricing — lives in the model catalog checked into the platform itself (`autogpt_platform/backend/backend/data/llm_registry/catalog.py`), so every install ships with the exact list its code supports. The platform comes pre-integrated with cutting-edge LLM providers:

* OpenAI - <https://openai.com/>
* Anthropic - <https://www.anthropic.com/>
* Groq - <https://groq.com/>
* Llama - <https://www.llama.com/>
* AI/ML API - <https://aimlapi.com/>
  * AI/ML API provides 300+ AI models including Deepseek, Gemini, ChatGPT. The models run at enterprise-grade rate limits and uptimes.

## License Overview

We've adopted a dual-license approach to balance open collaboration with sustainable development:

* **MIT License**: The majority of the AutoGPT repository remains under this license.
* **Polyform Shield License**: Applies to the new `autogpt_platform` folder.
* **Docker image**: The single-container image also bundles third-party services under their own licenses, including FalkorDB under SSPL-1.0. Their license files are in the image under `/usr/share/licenses/`.

This strategy allows us to share previously closed-source components, fostering a vibrant ecosystem of developers and users.

## Ready to Get Started?

* **Cloud:** Read the [Getting Started (Cloud)](getting-started-cloud.md) guide to start using the hosted platform immediately.
* **Self-Host:** Run the whole platform from one Docker image, `significantgravitas/autogpt` on Docker Hub, with the [single-container guide](single-container.md), or read the [Self-Hosting Guide](getting-started.md) to run each service with Docker Compose on your own infrastructure.

---

### Classic Original Autogpt Docs SUB AGENT REFACTOR PLAN (`classic/original_autogpt/docs/SUB_AGENT_REFACTOR_PLAN.md`)

# Sub-Agent Spawning Architecture Refactor

## Overview

This document outlines a comprehensive refactor to enable prompt strategies to spawn and coordinate sub-agents. This enables advanced patterns like:

- **LATS** (Language Agent Tree Search) - parallel exploration branches
- **Multi-agent debate** - consensus through agent interaction
- **Hierarchical decomposition** - delegate subtasks to specialists
- **Agent-as-tool** - use agents like functions

## Current Architecture (Before)

```
┌─────────────────────────────────────────────────────────────────┐
│                         Main Loop                                │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │ while running:                                           │    │
│  │   prompt = strategy.build_prompt(messages, task, ...)   │    │
│  │   response = llm.call(prompt)                           │    │
│  │   proposal = strategy.parse_response(response)          │    │
│  │   result = agent.execute(proposal)  ← tools only        │    │
│  └─────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘

Strategy has NO access to:
- Agent factory
- LLM provider
- File storage
- Execution context
- Other agents
```

## Proposed Architecture (After)

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

## Phase 1: Core Infrastructure

### 1.1 ExecutionContext Model

**File: `forge/agent/execution_context.py`** (NEW)

```
/* Detailed source-code truncated for AI context efficiency. */
```

### 1.2 AgentFactory Protocol

**File: `forge/agent/factory.py`** (NEW)

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

## Phase 2: Strategy Interface Updates

### 2.1 Update BasePromptStrategyConfiguration

**File: `original_autogpt/autogpt/agents/prompt_strategies/base.py`**

```python
class BasePromptStrategyConfiguration(SystemConfiguration):
    # ... existing fields ...

    # Sub-agent configuration
    enable_sub_agents: bool = UserConfigurable(default=False)
    max_sub_agents: int = UserConfigurable(default=5)
    sub_agent_timeout_seconds: int = UserConfigurable(default=300)
```

### 2.2 Update BaseMultiStepPromptStrategy

**File: `original_autogpt/autogpt/agents/prompt_strategies/base.py`**

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

## Phase 3: Agent Integration

### 3.1 Update Agent Class

**File: `original_autogpt/autogpt/agents/agent.py`**

```python
class Agent(BaseAgent[AnyActionProposal], Configurable[AgentSettings]):

    def __init__(
        self,
        settings: AgentSettings,
        llm_provider: MultiProvider,
        file_storage: FileStorage,
        app_config: AppConfig,
        permission_manager: Optional[CommandPermissionManager] = None,
        execution_context: Optional[ExecutionContext] = None,  # NEW
    ):
        super().__init__(settings, permission_manager=permission_manager)

        self.llm_provider = llm_provider
        self.app_config = app_config

        # Create or use provided execution context
        if execution_context:
            self.execution_context = execution_context
        else:
            # Root agent - create new context
            from forge.agent.factory import DefaultAgentFactory
            self.execution_context = ExecutionContext(
                llm_provider=llm_provider,
                file_storage=file_storage,
                agent_factory=DefaultAgentFactory(app_config),
            )

        # Create strategy and inject context
        self.prompt_strategy = self._create_prompt_strategy(app_config)
        if hasattr(self.prompt_strategy, 'set_execution_context'):
            self.prompt_strategy.set_execution_context(self.execution_context)

        # ... rest of __init__ ...
```

### 3.2 Update Agent Factory

**File: `original_autogpt/autogpt/agent_factory/configurators.py`**

```python
def create_agent(
    agent_id: str,
    task: str,
    app_config: AppConfig,
    file_storage: FileStorage,
    llm_provider: MultiProvider,
    ai_profile: Optional[AIProfile] = None,
    directives: Optional[AIDirectives] = None,
    permission_manager: Optional[CommandPermissionManager] = None,
    execution_context: Optional[ExecutionContext] = None,  # NEW
) -> Agent:
    # ... existing code ...

    return Agent(
        settings=agent_state,
        llm_provider=llm_provider,
        file_storage=file_storage,
        app_config=app_config,
        permission_manager=permission_manager,
        execution_context=execution_context,  # NEW
    )
```

---

## Phase 4: Example Strategy - LATS

### 4.1 LATS Strategy Implementation

**File: `original_autogpt/autogpt/agents/prompt_strategies/lats.py`** (NEW)

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

## Phase 5: Additional Infrastructure

### 5.1 Sub-Agent Communication Protocol

**File: `forge/agent/sub_agent_protocol.py`** (NEW)

```python
"""Protocol for sub-agent communication."""

from enum import Enum
from typing import Any, Optional
from pydantic import BaseModel


class MessageType(str, Enum):
    REQUEST = "request"
    RESPONSE = "response"
    STATUS = "status"
    CANCEL = "cancel"


class SubAgentMessage(BaseModel):
    """Message passed between parent and sub-agent."""

    type: MessageType
    sender_id: str
    recipient_id: str
    content: Any
    correlation_id: Optional[str] = None


class SubAgentRequest(SubAgentMessage):
    """Request from parent to sub-agent."""

    type: MessageType = MessageType.REQUEST
    task: str
    context: dict[str, Any] = {}


class SubAgentResponse(SubAgentMessage):
    """Response from sub-agent to parent."""

    type: MessageType = MessageType.RESPONSE
    success: bool
    result: Any
    error: Optional[str] = None
```

### 5.2 Resource Tracking Component

**File: `forge/components/resource_tracker.py`** (NEW)

```python
"""Component for tracking resource usage across agent hierarchy."""

from forge.agent.components import AgentComponent
from forge.agent.protocols import AfterExecute
from forge.models.action import ActionResult


class ResourceTrackerComponent(AgentComponent, AfterExecute):
    """Tracks resource usage for budget enforcement."""

    def __init__(self, execution_context):
        self.context = execution_context
        self.tokens_used = 0
        self.cycles_completed = 0

    def after_execute(self, result: ActionResult) -> None:
        self.cycles_completed += 1

        # Check budget
        budget = self.context.budget
        if budget.max_cycles and self.cycles_completed >= budget.max_cycles:
            raise BudgetExceededError("Cycle budget exceeded")
```

---

## Implementation Order

### Week 1: Core Infrastructure
1. Create `ExecutionContext` model
2. Create `AgentFactory` protocol
3. Update `BaseMultiStepPromptStrategy` with sub-agent methods
4. Update `Agent.__init__` to accept context

### Week 2: Integration
5. Update agent factory functions
6. Add sub-agent communication protocol
7. Create resource tracking component
8. Add tests for sub-agent spawning

### Week 3: Example Strategy
9. Implement LATS strategy skeleton
10. Implement MCTS core logic
11. Integrate sub-agent simulation
12. End-to-end testing

### Week 4: Polish
13. Error handling and cleanup
14. Documentation
15. Performance optimization
16. Additional example strategies

---

## Migration Notes

### Backward Compatibility

- Existing strategies continue to work unchanged
- `enable_sub_agents=False` by default
- ExecutionContext is optional (created automatically for root agents)

### Breaking Changes

- None for existing code
- New strategies using sub-agents require updated Agent class

### Testing Strategy

1. Unit tests for ExecutionContext
2. Integration tests for sub-agent lifecycle
3. LATS strategy tests with mocked sub-agents
4. End-to-end tests with real LLM calls

---

## Open Questions

1. **Shared State**: Should sub-agents share file storage? Separate workspaces?
2. **Permissions**: Inherit from parent or independent permission checks?
3. **History**: Should parent see sub-agent action history?
4. **Cancellation**: How to handle partial results when cancelled?
5. **Debugging**: How to trace execution across agent hierarchy?

---

### Content Challenges Beat (`docs/content/challenges/beat.md`)

# Beat a Challenge

If you have a solution or idea to tackle an existing challenge, you can contribute by working on it and submitting your solution. Here's how to get started:

## Guidelines for Beating a Challenge

1. **Choose a challenge**: Browse the [List of Challenges](list.md) and choose one that interests you or aligns with your expertise.

2. **Understand the problem**: Make sure you thoroughly understand the problem at hand, its scope, and the desired outcome.

3. **Develop a solution**: Work on creating a solution for the challenge. This may/

---

### Content Challenges Building Challenges (`docs/content/challenges/building_challenges.md`)

# Creating Challenges for AutoGPT

🏹 We're on the hunt for talented Challenge Creators! 🎯

Join us in shaping the future of AutoGPT by designing challenges that test its limits. Your input will be invaluable in guiding our progress and ensuring that we're on the right track. We're seeking individuals with a diverse skill set, including:

🎨 UX Design: Your expertise will enhance the user experience for those attempting to conquer our challenges. With your help, we'll develop a dedicated section in our wiki, and potentially even launch a standalone website.

💻 Coding Skills: Proficiency in Python, pytest, and VCR (a library that records OpenAI calls and stores them) will be essential for creating engaging and robust challenges.

⚙️ DevOps Skills: Experience with CI pipelines in GitHub and possibly Google Cloud Platform will be instrumental in streamlining our operations.

Are you ready to play a pivotal role in AutoGPT's journey? Apply now to become a Challenge Creator by opening a PR! 🚀


# Getting Started
Clone the original AutoGPT repo and checkout to master branch


The challenges are not written using a specific framework. They try to be very agnostic
The challenges are acting like a user that wants something done: 
INPUT:
- User desire
- Files, other inputs

Output => Artifact (files, image, code, etc, etc...)

## Defining your Agent

Go to https://github.com/Significant-Gravitas/AutoGPT/blob/master/classic/original_autogpt/tests/integration/agent_factory.py

Create your agent fixture.

```python
def kubernetes_agent(
    agent_test_config, workspace: Workspace
):
    # Please choose the commands your agent will need to beat the challenges, the full list is available in the main.py
    # (we 're working on a better way to design this, for now you have to look at main.py)
    command_registry = CommandRegistry()
    command_registry.import_commands("autogpt.commands.file_operations")
    command_registry.import_commands("autogpt.app")

    # Define all the settings of our challenged agent
    ai_profile = AIProfile(
        ai_name="Kubernetes",
        ai_role="an autonomous agent that specializes in creating Kubernetes deployment templates.",
        ai_goals=[
            "Write a simple kubernetes deployment file and save it as a kube.yaml.",
        ],
    )
    ai_profile.command_registry = command_registry

    system_prompt = ai_profile.construct_full_prompt()
    agent_test_config.set_continuous_mode(False)
    agent = Agent(
        command_registry=command_registry,
        config=ai_profile,
        next_action_count=0,
        triggering_prompt=DEFAULT_TRIGGERING_PROMPT,
    )

    return agent
```

## Creating your challenge
Go to `tests/challenges`and create a file that is called `test_your_test_description.py` and add it to the appropriate folder. If no category exists you can create a new one.

Your test could look something like this 

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

### Content Challenges Challenge Template (`docs/content/challenges/challenge_template.md`)

# Challenge Title

## Description

Provide a clear and concise description of the challenge. Include any relevant examples or files to illustrate the problem.

## Input

If the challenge involves specific input files, describe them here. Provide the file names and their contents, if necessary. Use triple backticks (```) to format the content as a code block.

For example:

instructions_1.txt

The current task_id is 4563.\n[NOISE intended to confuse the agent]
Read the file instructions_2.txt using the read_file command.

## Scope

Define the scope of the challenge, including any relevant constraints, requirements, or limitations.

## Success Evaluation

Explain how success will be measured or evaluated for the challenge. This helps others understand what the desired outcome is and how to work towards it.

---

### Content Challenges Introduction (`docs/content/challenges/introduction.md`)

# Introduction to Challenges

Welcome to the AutoGPT Challenges page! This is a space where we encourage community members to collaborate and contribute towards improving AutoGPT by identifying and solving challenges that AutoGPT is not yet able to achieve.

## What are challenges?

Challenges are tasks or problems that AutoGPT has difficulty solving or has not yet been able to accomplish. These may include improving specific functionalities, enhancing the model's understanding of specific domains, or even developing new features that the current version of AutoGPT lacks.

## Why are challenges important?

Addressing challenges helps us improve AutoGPT's performance, usability, and versatility. By working together to tackle these challenges, we can create a more powerful and efficient tool for everyone. It also allows the community to actively contribute to the project, making it a true open-source effort.

## How can you participate?

There are two main ways to get involved with challenges:

1. **Submit a Challenge**: If you have identified a task that AutoGPT struggles with, you can submit it as a challenge. This allows others to see the issue and collaborate on finding a solution.
2. **Beat a Challenge**: If you have a solution or idea to tackle an existing challenge, you can contribute by working on the challenge and submitting your solution.

To learn more about submitting and beating challenges, please visit the [List of Challenges](list.md), [Submit a Challenge](submit.md), and [Beat a Challenge](beat.md) pages.

We look forward to your contributions and the exciting solutions that the community will develop together to make AutoGPT even better!

!!! warning
    
    We're slowly transitioning to agbenchmark. agbenchmark is a simpler way to improve AutoGPT. Simply run:
    
    ```
    agbenchmark
    ```
    
    and beat as many challenges as possible.

For more agbenchmark options, look at the [readme](https://github.com/Significant-Gravitas/Auto-GPT-Benchmarks/tree/master/agbenchmark).

---

### Content Challenges List (`docs/content/challenges/list.md`)

# List of Challenges

This page contains a curated list of challenges that AutoGPT currently faces. If you think you have a solution or idea to tackle any of these challenges, feel free to dive in and start working on them! New challenges can also be submitted by following the guidelines on the [Submit a Challenge](./submit.md) page.

Memory Challenges: [List of Challenges](memory/introduction.md)

---

### Content Challenges Submit (`docs/content/challenges/submit.md`)

# Submit a Challenge

If you have identified a task or problem that AutoGPT struggles with, you can submit it as a challenge for the community to tackle. Here's how you can submit a new challenge:

## How to Submit a Challenge

1. Create a new `.md` file in the `challenges` directory in the AutoGPT GitHub repository. Make sure to pick the right category. 
2. Name the file with a descriptive title for the challenge, using hyphens instead of spaces (e.g., `improve-context-understanding.md`).
3. In the file, follow the [challenge_template.md](challenge_template.md) to describe the problem, define the scope, and evaluate success.
4. Commit the file and create a pull request.

Once submitted, the community can review and discuss the challenge. If deemed appropriate, it will be added to the [List of Challenges](list.md).

If you're looking to contribute by working on an existing challenge, check out [Beat a Challenge](beat.md) for guidelines on how to get started.

---

### Content Classic Share Your Logs (`docs/content/classic/share-your-logs.md`)

# Share your logs with us to help improve AutoGPT

Do you notice weird behavior with your agent? Do you have an interesting use case? Do you have a bug you want to report?
Follow the steps below to enable your logs and upload them. You can include these logs when making an issue report or discussing an issue with us.

## Enable Debug Logs

Activity, Error, and Debug logs are located in `./logs`

To print out debug logs:

```shell
./autogpt.sh --debug     # on Linux / macOS

.\autogpt.bat --debug    # on Windows

docker compose run --rm auto-gpt --debug    # in Docker
```

## Inspect and share logs

You can inspect and share logs via [e2b](https://e2b.dev).


1. Go to [autogpt.e2b.dev](https://autogpt.e2b.dev) and sign in.
2. You'll see logs from other members of the AutoGPT team that you can inspect.
3. Or you upload your own logs. Click on the "Upload log folder" button and select the debug logs dir that you generated. Wait a 1-2 seconds and the page reloads.
4. You can share logs via sharing the URL in your browser.


### Add tags to logs

You can add custom tags to logs for other members of your team. This is useful if you want to indicate that the agent is for example having issues with challenges.

E2b offers 3 types of severity:

- Success
- Warning
- Error

You can name your tag any way you want.

#### How to add a tag

1. Click on the "plus" button on the left from the logs folder name.

    

1. Type the name of a new tag.

1. Select the severity.

---

### Content Classic Testing (`docs/content/classic/testing.md`)

# Running tests

To run all tests, use the following command:

```shell
pytest
```

If `pytest` is not found:

```shell
python -m pytest
```

### Running specific test suites

- To run without integration tests:

```shell
pytest --without-integration
```

- To run without *slow* integration tests:

```shell
pytest --without-slow-integration
```

- To run tests and see coverage:

```shell
pytest --cov=autogpt --without-integration --without-slow-integration
```

## Running the linter

This project uses [flake8](https://flake8.pycqa.org/en/latest/) for linting.
We currently use the following rules: `E303,W293,W291,W292,E305,E231,E302`.
See the [flake8 rules](https://www.flake8rules.com/) for more information.

To run the linter:

```shell
flake8 .
```

Or:

```shell
python -m flake8 .
```

---

### Content Classic Usage (`docs/content/classic/usage.md`)

# AutoGPT Agent User Guide

!!! note
    This guide assumes you are in the `autogpt` folder, where the AutoGPT Agent
    is located.

## Command Line Interface

Running `./autogpt.sh` (or any of its subcommands) with `--help` lists all the possible
sub-commands and arguments you can use:

```shell
$ ./autogpt.sh --help
Usage: python -m autogpt [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  run    Sets up and runs an agent, based on the task specified by the...
  serve  Starts an Agent Protocol compliant AutoGPT server, which creates...
```

!!! important "For Windows users"
    On Windows, use `.\autogpt.bat` instead of `./autogpt.sh`.
    Everything else (subcommands, arguments) should work the same.

!!! info "Usage with Docker"
    For use with Docker, replace the script in the examples with
    `docker compose run --rm auto-gpt`:

    ```shell
    docker compose run --rm auto-gpt --ai-settings <filename>
    docker compose run --rm auto-gpt serve
    ```

### `run` &ndash; CLI mode

The `run` sub-command starts AutoGPT with the legacy CLI interface.

<details>
<summary>
<code>./autogpt.sh run --help</code>
</summary>

```
/* Detailed source-code truncated for AI context efficiency. */
```
</details>

This mode allows running a single agent, and saves the agent's state when terminated.
This means you can *resume* agents at a later time. See also [agent state].

!!! note
    For legacy reasons, the CLI will default to the `run` subcommand when none is
    specified: running `./autogpt.sh run [OPTIONS]` does the same as `./autogpt.sh [OPTIONS]`,
    but this may change in the future.

#### 💀 Continuous Mode ⚠️

Run the AI **without** user authorization, 100% automated.
Continuous mode is NOT recommended.
It is potentially dangerous and may cause your AI to run forever or carry out actions you would not usually authorize.
Use at your own risk.

```shell
./autogpt.sh --continuous
```

To exit the program, press ++ctrl+c++

### `serve` &ndash; Agent Protocol mode with UI

With `serve`, the application exposes an Agent Protocol compliant API and serves a
frontend, by default on `http://localhost:8000`. You can configure the port it is served on with the `AP_SERVER_PORT` environment variable.

<details>
<summary>
<code>./autogpt.sh serve --help</code>
</summary>

```shell
$ ./autogpt.sh serve --help
Usage: python -m autogpt serve [OPTIONS]

  Starts an Agent Protocol compliant AutoGPT server, which creates a custom
  agent for every task.

Options:
  --debug                     Enable Debug Mode
  --gpt3only                  Enable GPT3.5 Only Mode
  --gpt4only                  Enable GPT4 Only Mode
  --install-plugin-deps       Installs external dependencies for 3rd party
                              plugins.
  --help                      Show this message and exit.
```
</details>

For more information about the API of the application, see [agentprotocol.ai](https://agentprotocol.ai).



### Arguments

!!! attention
    Most arguments are equivalent to configuration options. See [`.env.template`][.env.template]
    for all available configuration options.

!!! note
    Replace anything in angled brackets (<>) to a value you want to specify

Here are some common arguments you can use when running AutoGPT:

* Run AutoGPT with a different AI Settings file

    ```shell
    ./autogpt.sh --ai-settings <filename>
    ```

* Run AutoGPT with a different Prompt Settings file

    ```shell
    ./autogpt.sh --prompt-settings <filename>
    ```

!!! note
    There are shorthands for some of these flags, for example `-P` for `--prompt-settings`.  
    Use `./autogpt.sh --help` for more information.

[.env.template]: https://github.com/Significant-Gravitas/AutoGPT/tree/master/classic/original_autogpt/.env.template

## Agent State
[agent state]: #agent-state

The state of individual agents is stored in the `data/agents` folder. You can use this
in various ways:

* Resume your agent at a later time.
* Create "checkpoints" for your agent so you can always go back to specific points in
    its history.
* Share your agent!

## Workspace
[workspace]: #workspace

Agents can read and write files. This happens in the `workspace` folder, which
is in `data/agents/<agent_id>/`. Files outside of this folder can not be accessed by the
agent *unless* `RESTRICT_TO_WORKSPACE` is set to `False`.

!!! warning
    We do not recommend disabling `RESTRICT_TO_WORKSPACE`, unless AutoGPT is running in
    a sandbox environment where it couldn't do any damage (e.g. Docker or a VM).

## Logs

Activity, Error, and Debug logs are located in `logs`.

!!! tip
    Do you notice weird behavior with your agent? Do you have an interesting use case? Do you have a bug you want to report?
    Follow the step below to enable your logs. You can include these logs when making an issue report or discussing an issue with us.

To print out debug logs:

```shell
./autogpt.sh --debug
```

## Disabling Commands

The best way to disable commands is to disable or remove the [component][components] that provides them.
However, if you want to selectively disable some commands, you can use the `DISABLED_COMMANDS` config in your `.env`.
Put the names of the commands you want to disable, separated by commas.
You can find the list of commands in built-in components [here][commands].

For example, to disable python coding features, set it to the value below:

```ini
DISABLED_COMMANDS=execute_python_code,execute_python_file
```

[components]: ../forge/components/components.md
[commands]: ../forge/components/built-in-components.md

---

### Content Forge Get Started (`docs/content/forge/get-started.md`)

# AutoGPT Forge

## 🛠️ Build your own Agent 🛠️

!!! warning
    If you are trying to use AutoGPT this is not the tutorial for you! You need to use [this one](../classic/setup/index.md)

Forge is a ready-to-go template for *your* agent application. All the boilerplate code is already handled, letting you channel all your creativity into the things that set *your* agent apart.

---

### 🛠️ **Why AutoGPT Forge?**

- 💤 **No More Boilerplate!** - Don't let the mundane tasks stop you. Fork and build without the headache of starting from scratch!
- 🧠 **Brain-centric Development!** - All the tools you need so you can spend 100% of your time on what matters - crafting the brain of your AI!
- 🛠️ **Tooling ecosystem!** - We work with the best in class tools to bring you the best experience possible!

---

### 🚀 **Get Started!**

The best way to get started is to fork or download the AutoGPT repository and look at the example agent in `classic/forge/agent/forge_agent.py`.
This can work as a starting point for your own agent.
Agents are built using *components* which provide different functionality, see the [Component Introduction](./components/introduction.md). You can find built-in components in `classic/forge/components/`.

!!! warning
    The tutorial series below is out of date.

The getting started [tutorial series](https://aiedge.medium.com/autogpt-forge-e3de53cc58ec) will guide you through the process of setting up your project all the way through to building a generalist agent.  

1. [AutoGPT Forge: A Comprehensive Guide to Your First Steps](https://aiedge.medium.com/autogpt-forge-a-comprehensive-guide-to-your-first-steps-a1dfdf46e3b4)
2. [AutoGPT Forge: The Blueprint of an AI Agent](https://aiedge.medium.com/autogpt-forge-the-blueprint-of-an-ai-agent-75cd72ffde6)
3. [AutoGPT Forge: Interacting with your Agent](https://aiedge.medium.com/autogpt-forge-interacting-with-your-agent-1214561b06b)
4. [AutoGPT Forge: Crafting Intelligent Agent Logic](https://medium.com/@aiedge/autogpt-forge-crafting-intelligent-agent-logic-bc5197b14cb4)

---

### Engineering Releases Clay Rock V1 (`docs/engineering/releases/clay-rock-v1.md`)

# Clay & Rock V1

Repository baseline: `45275cbb0bb36aa9aadfd2688de94daab3a8456e` (public master inspected 23 September 2026).

Retargeted to `dev` at `bdb8806affd46e79d412163f94b903615cf677d9`. That branch expands the roster to 32 Experts; its additional roles and saved template cohorts need a follow-up copy/artwork review. Catalog defaults use the approved specialist artwork for Maria and Mina. The onboarding illustration also uses supplied transparent artwork for seven specialists.

## What changes

- Approved v1.1 neutral Otto, Maria and Mina artwork, shared across the product. All 72 image exports retain the source bytes; artwork uses contain sizing, density variants, PNG fallback and an accessible text fallback. No new body motion or expression assets.
- Template defaults and new hires receive the reviewed Maria/Mina appearance and corrected managed biographies. Existing hires keep their stored appearance and identity. Selection uses saved URLs, never a person's display name, job title, category or task state.
- Compact identities show names and functional roles without a repeated tagline; detailed specialist identities retain AI Expert disclosure. Includes Create an Expert wording and Appearance upload labels. Otto remains the personal Head of AI. Existing route names and tool identifiers remain compatible.
- Shared generation guidance for new Expert charters, onboarding recommendations and Expert chat: no seniority, human career, tenure or credential claims, and no blanket promises to replace people. This is prompt guidance, not proof that every model response will comply.
- Home shows the supplied status detail and next step. Expert work shows textual run guidance; detailed run badges use the same labels. Approval explanations can wrap instead of being visually clipped. Run enums and approval decisions are unchanged.
- Files navigation follows the Files page flag. Onboarding email source says Otto and links to `/copilot`; workflow, billing and unsubscribe destinations retain their purpose. Shared notification artwork uses versioned Otto PNGs.
- Expert appearance uploads accept PNG/JPEG/WebP up to 5 MB, retain file-signature validation and malware scanning, and receive fresh filenames. Creation and appearance updates no longer require AutoMod approval or Redis receipts. Updates retain authenticated ownership checks. Upload errors preserve saved appearances and creation drafts. Marketplace publication moderation is outside this change.
- Onboarding uses eight spaced nodes with transparent artwork and name-only labels. Transparent 512-pixel WebP assets total 1.11 MiB, down from 9.59 MiB of supplied PNGs. Otto supports explicit size and transparency choices; team cards and headers use the original circular artwork. Emails use a 320-pixel transparent PNG over their existing colored bands.

## Release dependencies and safe rollout

1. Ship the versioned public image files before, or together with, backend defaults and email templates. Older image paths remain available. Do not purge old assets referenced by existing users or emails.
2. Appearance uploads use the existing media storage and malware scanner. No AutoMod or approval-receipt configuration is required for them; existing marketplace moderation is unchanged.
3. No database migration or roster reseed is needed to show updated catalog defaults. The projection only replaces recorded old defaults on template rows; new hires persist the projected values. Existing hires are deliberately not rewritten by reads.
4. Do not run the roster seed as a cosmetic migration: it also synchronizes routines, preloads and other behavior. Saved avatars are excluded from reseed backfills, including unchanged old defaults. Its other presentation backfills read batches of 100 hires, compare each field with the previous template and guard each write against concurrent edits. Identity rescoping retains the existing recognized-default gate. Rescope retries use recorded pre-rescope presentation defaults when the template has already advanced, so interrupted runs resume without leaving legacy hires behind. Unknown legacy baselines still leave the hire unchanged rather than guessing which fields the owner edited. Database-backed migration tests still need the isolated integration stack.
5. Hire Experts, onboarding and Files remain separate feature cohorts. This change does not enable flags. Verify each intended release cohort before rollout.
6. Onboarding tour delivery is managed through MailerLite. Updating these repository templates does not update a live MailerLite campaign. Its source and published copy need a separate owner review.

No agent-run local browser tests or production data changes were made. The user manually reviewed onboarding, team, sidebar and chat pages. Only the local marketplace template catalog was populated for previewing; workflows were not seeded. Local DOM tests do not establish production availability. Production/development deployment parity and real email-client rendering remain release checks.

## V2, after review

- Approve artwork for the remaining roster Experts (30 after the merge from `dev`, up from 13 in the original audit); retain their existing assets for V1.
- Replace the legacy custom appearance picker and generator with the reviewed mineral library and its constraints. V1 does not claim that legacy Notion artwork meets Clay & Rock.
- Implement reviewed facial expressions while keeping the body still, with text, reason and next action remaining independently accessible.
- Add the broader appearance generation workflow, ownership/version metadata where needed, and cooldowns after product approval.
- Complete marketing/CMS and MailerLite artwork and copy work in their actual source systems.
- Consider a separately reviewed, reversible migration for untouched older hires; do not replace user-authored names, avatars, instructions or history.

## Validation record

The workspace `.context/` logs contain implementation checks. Current coverage includes saved appearance stability and fallbacks, template projection, conditional backfill, upload validation and scanning, avatar API compatibility, Files gating, creation flows and email destinations. Live credentials, schedules, permissions and billing were not exercised or changed.

The historical runs below predate the removal of appearance moderation; their moderation-specific tests were removed with that feature.

Completed local checks:

- Frontend formatting, lint and TypeScript: passed (existing lint warnings remain).
- Full frontend suite: 7,515 initially passed; the 14 failures were investigated and fixed. Final affected-suite run: all 321 tests across 22 files passed, including every initially failing file.
- Backend: 219 tests passed across presentation/backfill, moderation, avatar routes, media storage, notification rendering, onboarding recommendations and Expert context. Route tests used the existing API auth fixtures with a local test-user fixture; no database was started.
- Backend formatting/lint excluding the global type pass: passed. Type checking of every changed backend source file: zero errors.
- The repository-wide backend type pass reports four errors in untouched files: an optional signing-key type in `autogpt_libs/auth/jwt_utils_test.py:74`, and missing `playwright.sync_api` imports in three `test/fixtures/skills/webapp-testing/examples/` scripts. No Playwright/browser dependency was installed or run to resolve those examples.
- React Doctor: no reported issues in its changed-file scan (92/100).
- All 72 approved character image exports match the supplied pack byte for byte. `git diff --check` passed.

PR packaging keeps two original 1024-pixel PNG exports (Maria and Mina, each under 1 MB) through exact-path exceptions to the 500 KB added-file check. The manifest is excluded from the entropy scanner because its reviewed SHA-256 image checksums are public integrity metadata. Other files retain both checks, and the separate gitleaks scan remains enabled.

Review follow-up checks: 276 tool-schema checks, 120 Expert-tool/context tests, 104 avatar-route/moderation tests, 33 media/moderation tests, 6 presentation tests and 44 renderer tests passed. The full frontend CI suite passed on the initial PR; review fixes add flag-cohort, empty-title disclosure, create-upload query and error-recovery checks. React Doctor's latest scan reports one maintainability warning in the existing ThreadHeader component (84/100), with no security or correctness finding. Live deployment checks above remain outstanding.

---

### Engineering Releases Clay Rock V2.1 (`docs/engineering/releases/clay-rock-v2.1.md`)

# Clay & Rock 2.1: the whole Expert family

Follows [Clay & Rock V1](clay-rock-v1.md). Design reference: [expert-design-system](https://github.com/Significant-Gravitas/expert-design-system), Edition 9 (`expert-family-v2.1/`).

## Why

PR #14858 gave the remaining 31 Experts Codex-generated clay artwork with its own rules: cream caps and spots on heads, lighter and darker shades per person inside a category, 52 per-category recolors that swapped when a marketplace filter changed, a shifted palette (Marketing `#C45F36`, Sales gold, Support rose, Content on General's warm stone), and a generator that let users choose shade, cream placement and accent count. It also moved Maria off the approved v1.1 asset. Each of those contradicts the design system's category rules and construction rules.

## What changes

- All 34 identities (32 built-ins, Otto, General) render through the managed image renderer from versioned library paths. Otto, Maria and Mina keep their v1.1 files; the other 31 come from `expert-family-v2.1`: the v2 registry masters plus the promoted sculptural masters for Jules, Remy, Maya, Zara, Marco, Noor and Frankie, re-framed to the v2 tile framing and exported with the same size ladder (WebP 24–1024 px, PNG 24–512 px, hashes in `frontend/public/autogpt-characters/manifest.json`).
- The palette is the registry's: terracotta `#C47F5C`, ochre `#C9A35B`, sage `#A5B09A`, muted coral `#CB9182`, slate blue `#98AFC6`, olive `#AAA77A`, muted teal `#81AAA6`, charcoal `#777570`, warm stone `#B5ADA0`, Otto's lavender `#B6A4C8`. Category dots, chips, card bands and covers use it as a 24 % tint; the artwork is never tinted.
- An Expert's artwork and family color come from its identity, not from the active filter or its role text. Maria under Content is the same Maria as under Marketing. A custom appearance takes the first stored category for its surfaces and General with none.
- The retired `/experts/clay/v1`–`v5` files are removed; `avatar_catalog.json` maps every old default (Notion SVGs, roster SVGs, clay identity files and per-category variants) to its identity, and the five shared v1 category sheets to General. Reads resolve them at once; the roster seed moves hired copies that still sit on a retired default. Uploads and generated images are untouched.
- The picker offers a custom Expert the General fallback, an upload or a generated candidate, and a hired built-in its saved identity as well; never another Expert's face. The copilot raise tool no longer takes a palette; a raised Expert starts on General.
- Generation is constrained to the design system: category (fixed color), head volume, base, tilt, cream route on the lower form, expression. References are Maria, Mina and an accepted peer from the library (512 px exports, hash-checked). Output is an opaque 1024 px studio tile. `EXPERT_AVATAR_MODEL` now defaults to the pinned `gpt-image-2-2026-04-21`.

## Release dependencies and safe rollout

1. Ship the `v2.1` public files with the backend catalog; the v1.1 files stay. No database migration.
2. Run the roster seed after deployment so hired copies on retired clay defaults move to their identity; reads already resolve them, so this is cosmetic consistency, not a prerequisite.
3. Generation needs the backend `OPENAI_API_KEY` and a model that supports image edits with several references and opaque PNG output. The first live call with the pinned snapshot has not been made from this change; verify it before enabling the feature cohort.
4. Human recognition of the promoted sculptural identities has not been measured. Promoting them is a product decision recorded in the design system on 25 September 2026, not a test result.

---

### Integrations Block Integrations Ai Condition (`docs/integrations/block-integrations/ai_condition.md`)

# AI Condition Block

## What it is
The AI Condition Block is a logical component that uses artificial intelligence to evaluate natural language conditions and produces outputs based on the result. This block allows you to define conditions in plain English rather than using traditional comparison operators.

## What it does
This block takes an input value and a natural language condition, then uses AI to determine whether the input satisfies the condition. Based on the result, it provides conditional outputs similar to a traditional if/else statement but with the flexibility of natural language evaluation.

## How it works
The block uses a Large Language Model (LLM) to evaluate the condition by:
1. Converting the input value to a string representation
2. Sending a carefully crafted prompt to the AI asking it to evaluate whether the input meets the specified condition
3. Parsing the AI's response to determine a true/false result
4. Outputting the appropriate value based on the result

## Inputs
| Input | Description |
|-------|-------------|
| Input Value | The value to be evaluated (can be text, number, or any data type) |
| Condition | A plaintext English description of the condition to evaluate |
| Yes Value | (Optional) The value to output if the condition is true. If not provided, Input Value will be used |
| No Value | (Optional) The value to output if the condition is false. If not provided, Input Value will be used |
| Model | The LLM model to use for evaluation (defaults to GPT-4o) |
| Credentials | API credentials for the LLM provider |

## Outputs
| Output | Description |
|--------|-------------|
| Result | A boolean value (true or false) indicating whether the condition was met |
| Yes Output | The output value if the condition is true. This will be the Yes Value if provided, or Input Value if not |
| No Output | The output value if the condition is false. This will be the No Value if provided, or Input Value if not |
| Error Message | Error message if the AI evaluation is uncertain or fails (empty string if successful) |

## Examples

### Email Address Validation
- **Input Value**: `"john@example.com"`
- **Condition**: `"the input is an email address"`
- **Result**: `true`
- **Yes Output**: `"john@example.com"` (or custom Yes Value)

### Geographic Location Check
- **Input Value**: `"San Francisco"`
- **Condition**: `"the input is a city in the USA"`
- **Result**: `true`
- **Yes Output**: `"San Francisco"` (or custom Yes Value)

### Error Detection
- **Input Value**: `"Error: Connection timeout"`
- **Condition**: `"the input is an error message or refusal"`
- **Result**: `true`
- **Yes Output**: `"Error: Connection timeout"` (or custom Yes Value)

### Content Classification
- **Input Value**: `"This is a detailed explanation of how machine learning works..."`
- **Condition**: `"the input is the body of an email"`
- **Result**: `false` (it's more like article content)
- **No Output**: Custom No Value or the input value

## Possible Use Cases
- **Content Classification**: Automatically classify text content (emails, articles, comments, etc.)
- **Data Validation**: Validate input data using natural language rules
- **Smart Routing**: Route data through different paths based on AI-evaluated conditions
- **Quality Control**: Check if content meets certain quality or format standards
- **Language Detection**: Determine if text is in a specific language or style
- **Sentiment Analysis**: Evaluate if content has positive, negative, or neutral sentiment
- **Error Handling**: Detect and route error messages or problematic inputs

## Advantages over Traditional Condition Blocks
- **Flexibility**: Can handle complex, nuanced conditions that would be difficult to express with simple comparisons
- **Natural Language**: Uses everyday language instead of programming logic
- **Context Awareness**: AI can understand context and meaning, not just exact matches
- **Adaptability**: Can handle variations in input format and wording

## Considerations
- **Performance**: Requires an API call to an LLM, which adds latency compared to traditional conditions
- **Cost**: Each evaluation consumes LLM tokens, which has associated costs
- **Reliability**: AI responses may occasionally be inconsistent, so critical logic should include fallback handling
- **Network Dependency**: Requires internet connectivity to access the LLM API

---

### Integrations Block Integrations Ai Shortform Video Block (`docs/integrations/block-integrations/ai_shortform_video_block.md`)

# AI Shortform Video Creator

## What it is
The AI Shortform Video Creator is a tool that generates short-form videos using artificial intelligence and various customization options.

## What it does
This block creates short videos by combining AI-generated visuals, narration, and background music based on user input. It can produce different styles of videos, including stock videos, moving AI images, or AI-generated videos.

## How it works
The block takes user input for script, visual style, audio, and other parameters. It then sends this information to the revid.ai API, which processes the request and generates the video. The block monitors the video creation process and provides the final video URL once it's ready.

## Inputs
| Input | Description |
|-------|-------------|
| API Key | Your revid.ai API key for authentication |
| Script | The text content for the video, including spoken narration and visual directions |
| Ratio | The aspect ratio of the video (e.g., "9 / 16" for vertical videos) |
| Resolution | The video resolution (e.g., "720p") |
| Frame Rate | The number of frames per second in the video |
| Generation Preset | The visual style for AI-generated content (e.g., "Default", "Anime", "Realist") |
| Background Music | The choice of background music track |
| Voice | The AI voice to use for narration |
| Video Style | The type of visual media to use (stock videos, moving AI images, or AI video) |

## Outputs
| Output | Description |
|--------|-------------|
| Video URL | The web address where the created video can be accessed |
| Error | A message explaining any issues that occurred during video creation (if applicable) |

## Possible use case
A social media marketer could use this block to quickly create engaging short-form videos for platforms like TikTok or Instagram Reels. They could input a script about a new product, choose a suitable visual style and background music, and get a professional-looking video without needing advanced video editing skills.

---

### Integrations Block Integrations Basic (`docs/integrations/block-integrations/basic.md`)

# Basic

Core utility blocks for storing values, printing output, file operations, type conversion, and basic data manipulation.


## Add Memory

### What it is
Add new memories to Mem0 with user segmentation

### How it works

This block integrates with Mem0, a memory layer service that stores and retrieves information across conversations. When you add a memory, the content is stored with the user's context and can optionally be segmented by run or agent, allowing for scoped memory retrieval later.

The block accepts either plain text or structured message objects (like those from AI blocks). You can attach metadata to memories for better organization and filtering. Memories persist across workflow executions, enabling your agents to "remember" past interactions.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| content | Content to add - either a string or list of message objects as output from an AI block | Content | No |
| metadata | Optional metadata for the memory | Dict[str, Any] | No |
| limit_memory_to_run | Limit the memory to the run | bool | No |
| limit_memory_to_agent | Limit the memory to the agent | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| action | Action of the operation | str |
| memory | Memory created | str |
| results | List of all results from the operation | List[Dict[str, str]] |

### Possible use case

**Personalized Assistants**: Store user preferences, past interactions, or learned information so your AI agent can provide personalized responses in future conversations.

**Context Carryover**: Save important details from one workflow run (like customer issues or project context) to reference in subsequent runs without asking the user again.

**Knowledge Building**: Accumulate facts and insights over time, creating a growing knowledge base that improves your agent's helpfulness with each interaction.


---

## Add To Dictionary

### What it is
Adds a new key-value pair to a dictionary. If no dictionary is provided, a new one is created.

### How it works

This block adds one or more key-value pairs to a dictionary. If you don't provide an existing dictionary, it creates a new one. You can add entries one at a time using the key/value fields, or add multiple entries at once using the entries field.

The block outputs the updated dictionary with all new entries added. This is useful for building up structured data objects as your workflow progresses, collecting information from multiple sources into a single data structure.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| dictionary | The dictionary to add the entry to. If not provided, a new dictionary will be created. | Dict[str, Any] | No |
| key | The key for the new entry. | str | No |
| value | The value for the new entry. | Value | No |
| entries | The entries to add to the dictionary. This is the batch version of the `key` and `value` fields. | Dict[str, Any] | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| updated_dictionary | The dictionary with the new entry added. | Dict[str, Any] |

### Possible use case

**Building API Payloads**: Construct complex JSON objects by adding fields from different workflow branches before sending to an API.

**Aggregating Form Data**: Collect user inputs from multiple form fields into a single structured object for processing or storage.

**Creating Configuration Objects**: Build up settings or configuration dictionaries dynamically based on conditional logic in your workflow.


---

## Add To List

### What it is
Adds a new entry to a list. The entry can be of any type. If no list is provided, a new one is created.

### How it works

This block appends items to a list or creates a new list if none is provided. You can add a single entry or multiple entries at once. The optional position parameter lets you insert items at a specific index rather than appending to the end.

Items can be of any type—strings, numbers, dictionaries, or other lists. This flexibility makes the block useful for building up collections of data as your workflow processes multiple items or accumulates results.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| list | The list to add the entry to. If not provided, a new list will be created. | List[Any] | No |
| entry | The entry to add to the list. Can be of any type (string, int, dict, etc.). | Entry | No |
| entries | The entries to add to the list. This is the batch version of the `entry` field. | List[Any] | No |
| position | The position to insert the new entry. If not provided, the entry will be appended to the end of the list. | int | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| updated_list | The list with the new entry added. | List[Any] |

### Possible use case

**Collecting Search Results**: Accumulate items from paginated API responses into a single list for batch processing or display.

**Building Email Recipients**: Gather email addresses from various sources into a recipient list before sending a message.

**Aggregating Errors**: Collect validation errors or warnings from multiple checks into a list for consolidated error reporting.


---

## Agent Date Input

### What it is
Block for date input.

### How it works

This block provides a date picker input field for users interacting with your agent. When the agent runs, users see a calendar widget to select a date, which is then passed to your workflow in YYYY-MM-DD format.

The block is part of the Agent Input family, allowing you to collect structured input from users at runtime rather than hardcoding values. This makes your agents interactive and reusable for different scenarios.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| name | The name of the input. | str | Yes |
| value | Date input (YYYY-MM-DD). | str (date) | No |
| title | The title of the input. | str | No |
| description | The description of the input. | str | No |
| advanced | Whether to show the input in the advanced section, if the field is not required. | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| result | Date result. | str (date) |

### Possible use case

**Appointment Scheduling**: Let users select a date for booking appointments, meetings, or reservations through your agent.

**Report Generation**: Allow users to specify a date range start or end point for generating custom reports.

**Deadline Setting**: Enable users to set due dates for tasks or projects when creating them through your workflow.


---

## Agent Dropdown Input

### What it is
Block for dropdown text selection.

### How it works

This block provides a dropdown selection input for users interacting with your agent. You define the available options using the `options` field, and users select one option from the list at runtime.

This is ideal when you want to constrain user input to a predefined set of choices, ensuring valid input and simplifying the user experience. The selected value is passed to downstream blocks in your workflow.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| name | The name of the input. | str | Yes |
| value | Text selected from a dropdown. | str | No |
| title | The title of the input. | str | No |
| description | The description of the input. | str | No |
| advanced | Whether to show the input in the advanced section, if the field is not required. | bool | No |
| options | If provided, renders the input as a dropdown selector restricted to these values. Leave empty for free-text input. | List[Any] | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| result | Selected dropdown value. | str |

### Possible use case

**Service Selection**: Let users choose from available service tiers (Basic, Pro, Enterprise) when configuring their request.

**Language Selection**: Allow users to select their preferred language from a list of supported options.

**Category Filtering**: Enable users to select a category (Sales, Support, Billing) to route their inquiry appropriately.


---

## Agent File Input

### What it is
Block for file upload input (string path for example).

### How it works

This block provides a file upload input for users interacting with your agent. Users can upload files which are stored temporarily and passed to your workflow as a file path reference.

By default, the block outputs a file path string that other blocks can use to access the uploaded file. The optional base64 mode converts the file content to base64 encoding, though using file paths is generally recommended for better performance with large files.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| name | The name of the input. | str | Yes |
| value | Path or reference to an uploaded file. | str (file) | No |
| title | The title of the input. | str | No |
| description | The description of the input. | str | No |
| advanced | Whether to show the input in the advanced section, if the field is not required. | bool | No |
| base_64 | Whether to produce output in base64 format (not recommended; you can pass the file reference across blocks). | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| result | File reference/path result. | str |

### Possible use case

**Document Processing**: Accept PDF or Word documents from users for analysis, summarization, or data extraction.

**Image Upload**: Allow users to upload images for processing, resizing, or AI-based analysis.

**Data Import**: Enable users to upload CSV or Excel files to import data into your workflow for processing.


---

## Agent Google Drive File Input

### What it is
Agent-level input for a Google Drive file. REQUIRED for any agent that reads or writes a Drive file (Sheets, Docs, Slides, or generic Drive) — the picker is the only source of the _credentials_id needed at runtime, so consuming blocks cannot receive a hardcoded ID. Set allowed_views to match the consumer: ["SPREADSHEETS"] for Sheets, ["DOCUMENTS"] for Docs, ["PRESENTATIONS"] for Slides (leave default for generic Drive). Wire `result` to the consumer block's Drive field and leave that field unset in the consumer's input_default. Example link to a Google Sheets block: {"source_name": "result", "sink_name": "spreadsheet"} (use "document" for Docs, "presentation" for Slides). Use one input block per distinct file; multiple consumers of the same file share it.

### How it works

This block integrates with Google Drive to let users select files directly from their connected Drive account. The Google Drive file picker appears at runtime, allowing users to browse and select files without manually copying file IDs or URLs.

You can configure which file types to display (documents, spreadsheets, presentations) using the allowed_views option. The block outputs the selected file's metadata including its ID, name, and URL for use by other Google-integrated blocks.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| name | The name of the input. | str | Yes |
| value | The selected Google Drive file. | GoogleDriveFile | No |
| title | The title of the input. | str | No |
| description | The description of the input. | str | No |
| advanced | Whether to show the input in the advanced section, if the field is not required. | bool | No |
| allowed_views | Which views to show in the file picker (DOCS, SPREADSHEETS, PRESENTATIONS, etc.). | List["DOCS" \| "DOCUMENTS" \| "SPREADSHEETS" \| "PRESENTATIONS" \| "DOCS_IMAGES" \| "FOLDERS"] | No |
| allow_folder_selection | Whether to allow selecting folders. | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| result | The selected Google Drive file with ID, name, URL, and other metadata. | GoogleDriveFile |

### Possible use case

**Document Workflow**: Let users select a Google Doc to read, analyze, or append content to without knowing the file ID.

**Spreadsheet Data Import**: Allow users to pick a Google Sheet to import data from for processing or analysis.

**File Organization**: Enable users to select folders or files for bulk operations like moving, copying, or organizing content.


---

## Agent Input

### What it is
A block that accepts and processes user input values within a workflow, supporting various input types and validation.

### How it works

It accepts a value from the user, along with metadata such as name and description. The block then outputs the provided value.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| name | The name of the input. | str | Yes |
| value | The value to be passed as input. | Value | No |
| title | The title of the input. | str | No |
| description | The description of the input. | str | No |
| advanced | Whether to show the input in the advanced section, if the field is not required. | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| result | The value passed as input. | Result |

### Possible use case

Collecting user preferences at the start of a personalized recommendation workflow.


---

## Agent Long Text Input

### What it is
Block for long text input (multi-line).

### How it works

This block provides a multi-line text area input for users interacting with your agent. Unlike the short text input, this displays a larger text area suitable for paragraphs, descriptions, or any content that may span multiple lines.

The block is ideal for collecting longer-form content like messages, descriptions, or code snippets from users at runtime. The text is passed as-is to downstream blocks in your workflow.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| name | The name of the input. | str | Yes |
| value | Long text input (potentially multi-line). | str (long-text) | No |
| title | The title of the input. | str | No |
| description | The description of the input. | str | No |
| advanced | Whether to show the input in the advanced section, if the field is not required. | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| result | Long text result. | str |

### Possible use case

**Feedback Collection**: Accept detailed user feedback, reviews, or comments that may require multiple paragraphs.

**Content Submission**: Let users submit articles, blog posts, or documentation content for processing or publication.

**Query Input**: Allow users to enter complex questions or prompts for AI processing that require detailed context.


---

## Agent Number Input

### What it is
Block for number input.

### How it works

This block provides a numeric input field for users interacting with your agent. The input validates that the user enters a valid integer, preventing text or invalid values from being submitted.

This is useful when you need numeric parameters like quantities, counts, limits, or any integer value from users at runtime. The number is passed to downstream blocks for use in calculations or configurations.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| name | The name of the input. | str | Yes |
| value | Number input. | int | No |
| title | The title of the input. | str | No |
| description | The description of the input. | str | No |
| advanced | Whether to show the input in the advanced section, if the field is not required. | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| result | Number result. | int |

### Possible use case

**Quantity Selection**: Let users specify how many items to process, order, or generate.

**Pagination Control**: Allow users to specify page numbers or result limits for data retrieval.

**Threshold Setting**: Enable users to set numeric thresholds or limits for alerts, filtering, or processing logic.


---

## Agent Output

### What it is
A block that records and formats workflow results for display to users, with optional Jinja2 template formatting support.

### How it works

It accepts an input value along with a name, description, and optional format string. If a format string is provided, it attempts to apply the formatting to the input value before outputting it.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| value | The value to be recorded as output. | Value | No |
| name | The name of the output. | str | Yes |
| title | The title of the output. | str | No |
| description | The description of the output. | str | No |
| format | The format string to be used to format the recorded_value. Use Jinja2 syntax. | str | No |
| escape_html | Whether to escape special characters in the inserted values to be HTML-safe. Enable for HTML output, disable for plain text. | bool | No |
| advanced | Whether to treat the output as advanced. | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| output | The value recorded as output. | Output |
| name | The name of the value recorded as output. | Name |

### Possible use case

Presenting the final results of a data analysis workflow in a specific format.


---

## Agent Short Text Input

### What it is
Block for short text input (single-line).

### How it works

This block provides a single-line text input field for users interacting with your agent. It's designed for brief text entries like names, titles, URLs, or short responses.

The input displays as a standard text field and passes the entered text to downstream blocks. Use this for collecting concise information that doesn't require multiple lines or extensive formatting.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| name | The name of the input. | str | Yes |
| value | Short text input. | str (short-text) | No |
| title | The title of the input. | str | No |
| description | The description of the input. | str | No |
| advanced | Whether to show the input in the advanced section, if the field is not required. | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| result | Short text result. | str |

### Possible use case

**Name Collection**: Gather user names, company names, or project names for personalization.

**Search Queries**: Accept search terms or keywords from users to drive search functionality.

**URL Input**: Let users provide URLs for websites, APIs, or resources to process in your workflow.


---

## Agent Table Input

### What it is
Block for table data input with customizable headers.

### How it works

This block provides a tabular data input interface for users interacting with your agent. Users can enter data in a spreadsheet-like table format with customizable column headers.

The table input is ideal for structured data entry where users need to provide multiple records with consistent fields. The block outputs the data as a list of dictionaries, with each row becoming a dictionary where column headers are keys.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| name | The name of the input. | str | Yes |
| value | The table data as a list of dictionaries. | List[Dict[str, Any]] | No |
| title | The title of the input. | str | No |
| description | The description of the input. | str | No |
| advanced | Whether to show the input in the advanced section, if the field is not required. | bool | No |
| column_headers | Column headers for the table. | List[str] | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| result | The table data as a list of dictionaries with headers as keys. | List[Dict[str, Any]] |

### Possible use case

**Bulk Data Entry**: Let users input multiple records at once, like a list of contacts with name, email, and phone columns.

**Order Processing**: Accept line items for an order with product, quantity, and price columns.

**Task Lists**: Allow users to submit multiple tasks with columns for title, assignee, and priority.


---

## Agent Time Input

### What it is
Block for time input.

### How it works

This block provides a time picker input for users interacting with your agent. Users select a time (hours, minutes, and optionally seconds) which is passed to your workflow in HH:MM:SS format.

The time picker provides a user-friendly interface for selecting times without requiring users to type in a specific format. This ensures valid time values and improves the user experience.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| name | The name of the input. | str | Yes |
| value | Time input (HH:MM:SS). | str (time) | No |
| title | The title of the input. | str | No |
| description | The description of the input. | str | No |
| advanced | Whether to show the input in the advanced section, if the field is not required. | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| result | Time result. | str (time) |

### Possible use case

**Appointment Scheduling**: Let users specify a time for meetings, calls, or appointments.

**Reminder Setting**: Allow users to set reminder times for notifications or alerts.

**Shift Configuration**: Enable users to define start or end times for work shifts or availability windows.


---

## Agent Toggle Input

### What it is
Block for boolean toggle input.

### How it works

This block provides a boolean toggle (on/off switch) input for users interacting with your agent. Users simply click to toggle between true and false states, making yes/no decisions quick and error-free.

The toggle is ideal for binary choices like enabling features, confirming actions, or setting preferences. The boolean value is passed to downstream blocks for conditional logic.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| name | The name of the input. | str | Yes |
| value | Boolean toggle input. | bool | No |
| title | The title of the input. | str | No |
| description | The description of the input. | str | No |
| advanced | Whether to show the input in the advanced section, if the field is not required. | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| result | Boolean toggle result. | bool |

### Possible use case

**Feature Flags**: Let users enable or disable optional features in your workflow.

**Confirmation Toggles**: Require users to acknowledge terms, confirm destructive actions, or opt into notifications.

**Mode Selection**: Allow users to switch between modes like "test mode" vs "production mode" or "verbose" vs "quiet" output.


---

## Block Installation

### What it is
Given a code string, this block allows the verification and installation of a block code into the system.

### How it works

This block allows dynamic installation of new block types into the system from Python code. The code is verified for safety and correctness before installation. Once installed, the new block becomes available for use in workflows.

This enables extensibility by allowing custom blocks to be added without modifying the core system, though it requires the code to follow the block specification format.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| code | Python code of the block to be installed | str | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the block installation fails | str |
| success | Success message if the block is installed successfully | str |

### Possible use case

**Custom Integrations**: Install blocks that connect to proprietary or internal APIs not covered by built-in blocks.

**Dynamic Workflows**: Allow administrators to extend workflow capabilities without redeploying the entire system.

**Experimental Features**: Test new block implementations before formally adding them to the block library.


---

## Concatenate Lists

### What it is
Concatenates multiple lists into a single list. All elements from all input lists are combined in order. Supports optional deduplication and None removal.

### How it works

The block iterates through each list in the input and extends a result list with all elements from each one. It processes lists in order, so `[[1, 2], [3, 4]]` becomes `[1, 2, 3, 4]`.

The block includes validation to ensure each item is actually a list. If a non-list value (like a string or number) is encountered, the block outputs an error message instead of proceeding. None values are skipped automatically.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| lists | A list of lists to concatenate together. All lists will be combined in order into a single list. | List[List[Any]] | Yes |
| deduplicate | If True, remove duplicate elements from the concatenated result while preserving order. | bool | No |
| remove_none | If True, remove None values from the concatenated result. | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if concatenation failed due to invalid input types. | str |
| concatenated_list | The concatenated list containing all elements from all input lists in order. | List[Any] |
| length | The total number of elements in the concatenated list. | int |

### Possible use case

**Paginated API Merging**: Combine results from multiple API pages into a single list for batch processing or display.

**Parallel Task Aggregation**: Merge outputs from parallel workflow branches that each produce a list of results.

**Multi-Source Data Collection**: Combine data collected from different sources (like multiple RSS feeds or API endpoints) into one unified list.


---

## Dictionary Is Empty

### What it is
Checks if a dictionary is empty.

### How it works

This block checks whether a dictionary has any entries and returns a boolean result. An empty dictionary (no key-value pairs) returns true, while a dictionary with any entries returns false.

This is useful for conditional logic where you need to verify if data was returned from an API, if user input was provided, or if a collection process yielded any results.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| dictionary | The dictionary to check. | Dict[str, Any] | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| is_empty | True if the dictionary is empty. | bool |

### Possible use case

**Response Validation**: Check if an API returned an empty response before processing data.

**Input Verification**: Verify that user-provided form data contains at least one field before submission.

**Conditional Processing**: Skip processing steps when no matching data was found in a search or filter operation.


---

## File Store

### What it is
Downloads and stores a file from a URL, data URI, or local path. Use this to fetch images, documents, or other files for processing. In CoPilot: saves to workspace (use list_workspace_files to see it). In graphs: outputs a data URI to pass to other blocks.

### How it works

This block takes a file from various sources (URL, data URI, or local path) and stores it in a temporary directory for use by other blocks in your workflow. This normalizes file handling regardless of the original source.

The block outputs a file path that other blocks can use to access the stored file. The optional base64 output mode is available but file paths are recommended for better performance with large files.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| file_in | The file to download and store. Can be a URL (https://...), data URI, or local path. | str (file) | Yes |
| base_64 | Whether to produce output in base64 format (not recommended, you can pass the file reference across blocks). | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| file_out | Reference to the stored file. In CoPilot: workspace:// URI (visible in list_workspace_files). In graphs: data URI for passing to other blocks. | str (file) |

### Possible use case

**URL File Download**: Fetch a file from a URL and make it available for local processing by other blocks.

**Data URI Conversion**: Convert base64-encoded data URIs (like from a web form) into accessible file paths.

**File Normalization**: Standardize file access across different input sources (URLs, uploads, local files) for consistent downstream processing.


---

## Find In Dictionary

### What it is
A block that looks up a value in a dictionary, list, or object by key or index and returns the corresponding value.

### How it works

This block extracts a value from a dictionary (object) or list using a key or index. If the key exists, the value is output through the "output" pin. If the key is missing, the original input is output through the "missing" pin.

This enables safe data access with built-in handling for missing keys, preventing workflow errors when expected data isn't present. You can use string keys for dictionaries or integer indices for lists.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| input | Dictionary to lookup from | Input | Yes |
| key | Key to lookup in the dictionary | str \| int | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| output | Value found for the given key | Output |
| missing | Value of the input that missing the key | Missing |

### Possible use case

**API Response Parsing**: Extract specific fields (like "data" or "results") from API response objects.

**Configuration Access**: Retrieve settings from a configuration dictionary by key name.

**User Data Extraction**: Pull specific user attributes (name, email, preferences) from a user profile object.


---

## Find In List

### What it is
Finds the index of the value in the list.

### How it works

This block searches a list for a specific value and returns its position (index). If found, it outputs the zero-based index and sets "found" to true. If not found, it outputs the original value through "not_found_value" and sets "found" to false.

This enables conditional logic based on list membership and helps locate items for subsequent list operations like replacement or removal.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| list | The list to search in. | List[Any] | Yes |
| value | The value to search for. | Value | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| index | The index of the value in the list. | int |
| found | Whether the value was found in the list. | bool |
| not_found_value | The value that was not found in the list. | Not Found Value |

### Possible use case

**Duplicate Detection**: Check if an item already exists in a list before adding it.

**Status Lookup**: Find if a value is in a list of valid states or allowed values.

**Position Finding**: Locate an item's position for subsequent operations like updates or removals.


---

## Flatten List

### What it is
Flattens a nested list structure into a single flat list. Supports configurable maximum flattening depth.

### How it works

This block recursively traverses a nested list and extracts all leaf elements into a single flat list. You can control how deep the flattening goes with the max_depth parameter: set it to -1 to flatten completely, or to a positive integer to flatten only that many levels.

The block also reports the original nesting depth of the input, which is useful for understanding the structure of data coming from sources with varying levels of nesting.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| nested_list | A potentially nested list to flatten into a single-level list. | List[Any] | Yes |
| max_depth | Maximum depth to flatten. -1 means flatten completely. 1 means flatten only one level. | int | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if flattening failed. | str |
| flattened_list | The flattened list with all nested elements extracted. | List[Any] |
| length | The number of elements in the flattened list. | int |
| original_depth | The maximum nesting depth of the original input list. | int |

### Possible use case

**Normalizing API Responses**: Flatten nested JSON arrays from different API endpoints into a uniform single-level list for consistent processing.

**Aggregating Nested Results**: Combine results from recursive file searches or nested category trees into a flat list of items for display or export.

**Data Pipeline Cleanup**: Simplify deeply nested data structures from multiple transformation steps into a clean flat list before final output.


---

## Get All Memories

### What it is
Retrieve all memories from Mem0 with optional conversation filtering

### How it works

This block retrieves all stored memories from Mem0 for the current user context. You can filter results by categories or metadata, and scope the retrieval to the current run or agent using the limit options.

Memories are returned as a list that your workflow can iterate through. This is useful for reviewing accumulated knowledge, debugging what your agent has learned, or aggregating past interactions for analysis.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| trigger | An unused field that is used to trigger the block when you have no other inputs | bool | No |
| categories | Filter by categories | List[str] | No |
| metadata_filter | Optional metadata filters to apply | Dict[str, Any] | No |
| limit_memory_to_run | Limit the memory to the run | bool | No |
| limit_memory_to_agent | Limit the memory to the agent | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| memories | List of memories | Memories |

### Possible use case

**Context Review**: Retrieve all memories at the start of a session to understand what your agent already knows about a user.

**Memory Export**: Collect all stored memories for backup, analysis, or migration to another system.

**Memory Management**: List all memories to identify outdated or incorrect information that needs updating.


---

## Get Latest Memory

### What it is
Retrieve the latest memory from Mem0 with optional key filtering

### How it works

This block retrieves the most recently stored memory from Mem0. You can filter by categories, metadata, or conversation ID to find the latest relevant memory. The block indicates whether a memory was found and returns it if available.

This is useful for quickly accessing the last piece of information stored without iterating through all memories, such as checking the most recent user preference or the last conversation topic.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| trigger | An unused field that is used to trigger the block when you have no other inputs | bool | No |
| categories | Filter by categories | List[str] | No |
| conversation_id | Optional conversation ID to retrieve the latest memory from (uses run_id) | str | No |
| metadata_filter | Optional metadata filters to apply | Dict[str, Any] | No |
| limit_memory_to_run | Limit the memory to the run | bool | No |
| limit_memory_to_agent | Limit the memory to the agent | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| memory | Latest memory if found | Dict[str, Any] |
| found | Whether a memory was found | bool |

### Possible use case

**Conversation Continuity**: Retrieve the last topic discussed to provide context when resuming a conversation.

**Status Tracking**: Get the most recent status update or progress report stored during a workflow.

**Quick Recall**: Access the last user preference or setting without loading the full memory history.


---

## Get List Item

### What it is
Returns the element at the given index.

### How it works

This block retrieves an item from a list at a specific index position. It uses zero-based indexing (first item is 0) and supports negative indices for accessing items from the end (e.g., -1 for the last item).

If the index is out of range, the block outputs an error. This is useful for accessing specific elements without iterating through the entire list.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| list | The list to get the item from. | List[Any] | Yes |
| index | The 0-based index of the item (supports negative indices). | int | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| item | The item at the specified index. | Item |

### Possible use case

**First/Last Item Access**: Get the first item (index 0) or last item (index -1) from a list of results.

**Ordered Selection**: Access a specific position in a ranked list, like the second-highest score or third most recent entry.

**Array Unpacking**: Extract individual elements from a fixed-structure list where each position has a known meaning.


---

## Get Weather Information

### What it is
Retrieves weather information for a specified location using OpenWeatherMap API.

### How it works

The block sends a request to a weather API (like OpenWeatherMap) with the provided location. It then processes the response to extract relevant weather data.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| location | Location to get weather information for | str | Yes |
| use_celsius | Whether to use Celsius or Fahrenheit for temperature | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the weather information cannot be retrieved | str |
| temperature | Temperature in the specified location | str |
| humidity | Humidity in the specified location | str |
| condition | Weather condition in the specified location | str |

### Possible use case

A travel planning application could use this block to provide users with current weather information for their destination cities.


---

## Human In The Loop

### What it is
Pause execution for human review. Data flows through approved_data or rejected_data output based on the reviewer's decision. Outputs contain the actual data, not status strings.

### How it works

This block pauses workflow execution and presents data to a human reviewer for approval. The workflow waits until the human approves or rejects the data, then routes to the corresponding output. If editable is enabled, the reviewer can modify the data before approving.

This enables human oversight at critical points in automated workflows, ensuring important decisions have human verification before proceeding.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| data | The data to be reviewed by a human user. This exact data will be passed through to either approved_data or rejected_data output based on the reviewer's decision. | Data | Yes |
| name | A descriptive name for what this data represents. This helps the reviewer understand what they are reviewing. | str | Yes |
| editable | Whether the human reviewer can edit the data before approving or rejecting it | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| approved_data | Outputs the input data when the reviewer APPROVES it. The value is the actual data itself (not a status string like 'APPROVED'). If the reviewer edited the data, this contains the modified version. Connect downstream blocks here for the 'approved' workflow path. | Approved Data |
| rejected_data | Outputs the input data when the reviewer REJECTS it. The value is the actual data itself (not a status string like 'REJECTED'). If the reviewer edited the data, this contains the modified version. Connect downstream blocks here for the 'rejected' workflow path. | Rejected Data |
| review_message | Optional message provided by the reviewer explaining their decision. Only outputs when the reviewer provides a message; this pin does not fire if no message was given. | str |

### Possible use case

**Content Moderation**: Review AI-generated content before publishing to ensure quality and appropriateness.

**Approval Workflows**: Require manager approval for actions like large purchases, access requests, or configuration changes.

**Quality Assurance**: Let reviewers verify data transformations or calculations before they're committed to production systems.


---

## Interleave Lists

### What it is
Interleaves elements from multiple lists in round-robin fashion, alternating between sources.

### How it works

This block takes elements from each input list in round-robin order, picking one element from each list in turn. For example, given `[[1, 2, 3], ['a', 'b', 'c']]`, it produces `[1, 'a', 2, 'b', 3, 'c']`.

When lists have different lengths, shorter lists stop contributing once exhausted, and remaining elements from longer lists continue to be added in order.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| lists | A list of lists to interleave. Elements will be taken in round-robin order. | List[List[Any]] | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if interleaving failed. | str |
| interleaved_list | The interleaved list with elements alternating from each input list. | List[Any] |
| length | The total number of elements in the interleaved list. | int |

### Possible use case

**Balanced Content Mixing**: Alternate between content from different sources (e.g., mixing promotional and organic posts) for a balanced feed.

**Round-Robin Scheduling**: Distribute tasks evenly across workers or queues by interleaving items from separate task lists.

**Multi-Language Output**: Weave together translated text segments with their original counterparts for side-by-side comparison.


---

## List Difference

### What it is
Computes the difference between two lists. Returns elements in the first list not found in the second, or symmetric difference.

### How it works

This block compares two lists and returns elements from list_a that do not appear in list_b. It uses hash-based lookup for efficient comparison. When symmetric mode is enabled, it returns elements that are in either list but not in both.

The order of elements from list_a is preserved in the output, and elements from list_b are appended when using symmetric difference.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| list_a | The primary list to check elements from. | List[Any] | Yes |
| list_b | The list to subtract. Elements found here will be removed from list_a. | List[Any] | Yes |
| symmetric | If True, compute symmetric difference (elements in either list but not both). | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed. | str |
| difference | Elements from list_a not found in list_b (or symmetric difference if enabled). | List[Any] |
| length | The number of elements in the difference result. | int |

### Possible use case

**Change Detection**: Compare a current list of records against a previous snapshot to find newly added or removed items.

**Exclusion Filtering**: Remove items from a list that appear in a blocklist or already-processed list to avoid duplicates.

**Data Sync**: Identify which items exist in one system but not another to determine what needs to be synced.


---

## List Intersection

### What it is
Computes the intersection of two lists, returning only elements present in both.

### How it works

This block finds elements that appear in both input lists by hashing elements from list_b for efficient lookup, then checking each element of list_a against that set. The output preserves the order from list_a and removes duplicates.

This is useful for finding common items between two datasets without needing to manually iterate or compare.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| list_a | The first list to intersect. | List[Any] | Yes |
| list_b | The second list to intersect. | List[Any] | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed. | str |
| intersection | Elements present in both list_a and list_b. | List[Any] |
| length | The number of elements in the intersection. | int |

### Possible use case

**Finding Common Tags**: Identify shared tags or categories between two items for recommendation or grouping purposes.

**Mutual Connections**: Find users or contacts that appear in both of two different lists, such as shared friends or overlapping team members.

**Feature Comparison**: Determine which features or capabilities are supported by both of two systems or products.


---

## List Is Empty

### What it is
Checks if a list is empty.

### How it works

This block checks whether a list contains any items and returns a boolean result. An empty list (no elements) returns true, while a list with any elements returns false.

This is useful for conditional logic where you need to verify if search results were found, if items are available for processing, or if a collection has any entries to iterate over.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| list | The list to check. | List[Any] | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| is_empty | True if the list is empty. | bool |

### Possible use case

**Search Result Handling**: Check if a search returned any results before processing, displaying "no results found" when empty.

**Batch Processing Guard**: Verify that a list has items before starting a batch operation to avoid empty iterations.

**Conditional Messaging**: Send different notifications based on whether pending items exist or the queue is empty.


---

## Note

### What it is
A visual annotation block that displays a sticky note in the workflow editor for documentation and organization purposes.

### How it works

It simply accepts a text input and passes it through as an output to be displayed as a note.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| text | The text to display in the sticky note. | str | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| output | The text to display in the sticky note. | str |

### Possible use case

Adding explanatory notes or reminders within a complex workflow to help users understand different stages or provide additional context.


---

## Print To Console

### What it is
A debugging block that outputs text to the console for monitoring and troubleshooting workflow execution.

### How it works

This block outputs the provided data to the server console log and passes it through as output. It's primarily used for debugging workflows by allowing you to inspect values at any point in the data flow.

The block accepts any data type and both prints it for debugging visibility and forwards it to downstream blocks, making it easy to insert into existing connections without disrupting the workflow.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| text | The data to print to the console. | Text | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| output | The data printed to the console. | Output |
| status | The status of the print operation. | str |

### Possible use case

**Workflow Debugging**: Insert at any point to inspect what data is flowing through that connection during testing.

**Variable Inspection**: Log the values of variables or API responses to understand what your workflow is receiving.

**Progress Tracking**: Add print statements at key stages to monitor workflow progress in the server logs.


---

## Remove From Dictionary

### What it is
Removes a key-value pair from a dictionary.

### How it works

This block removes a key-value pair from a dictionary by specifying the key. The updated dictionary without that entry is output. Optionally, you can retrieve the value that was removed by enabling return_value.

If the key doesn't exist in the dictionary, the operation may error or return the dictionary unchanged depending on the implementation. This is useful for cleaning up data or extracting and removing values in one step.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| dictionary | The dictionary to modify. | Dict[str, Any] | Yes |
| key | Key to remove from the dictionary. | str \| int | Yes |
| return_value | Whether to return the removed value. | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| updated_dictionary | The dictionary after removal. | Dict[str, Any] |
| removed_value | The removed value if requested. | Removed Value |

### Possible use case

**Data Cleaning**: Remove sensitive fields (like passwords or tokens) from data before logging or storing.

**Pop Pattern**: Extract and remove a value from a dictionary in a single operation, like dequeuing items.

**Object Trimming**: Remove unnecessary or deprecated fields from configuration objects before processing.


---

## Remove From List

### What it is
Removes an item from a list by value or index.

### How it works

This block removes an item from a list either by value (remove first occurrence) or by index (remove at specific position). Negative indices are supported for removal from the end. Optionally, the removed item can be returned.

This provides flexibility for both "remove this specific item" and "remove the item at this position" use cases in a single block.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| list | The list to modify. | List[Any] | Yes |
| value | Value to remove from the list. | Value | No |
| index | Index of the item to pop (supports negative indices). | int | No |
| return_item | Whether to return the removed item. | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| updated_list | The list after removal. | List[Any] |
| removed_item | The removed item if requested. | Removed Item |

### Possible use case

**Queue Processing**: Pop items from the front of a list to process them one at a time (FIFO queue).

**Exclusion Lists**: Remove specific values from a list, like filtering out certain options or invalid entries.

**Stack Operations**: Pop items from the end of a list for last-in-first-out processing.


---

## Replace Dictionary Value

### What it is
Replaces the value for a specified key in a dictionary.

### How it works

This block updates the value for an existing key in a dictionary. The old value is replaced with the new one, and the updated dictionary is output. The block also returns the old value that was replaced.

This is useful for updating specific fields in a data object while preserving all other fields, or for tracking what value was changed during an update operation.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| dictionary | The dictionary to modify. | Dict[str, Any] | Yes |
| key | Key to replace the value for. | str \| int | Yes |
| value | The new value for the given key. | Value | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| updated_dictionary | The dictionary after replacement. | Dict[str, Any] |
| old_value | The value that was replaced. | Old Value |

### Possible use case

**Status Updates**: Change the status field in a record from "pending" to "completed" while preserving all other data.

**Configuration Changes**: Update a single setting in a configuration object without rebuilding the entire config.

**Field Transformations**: Replace a raw value with a processed or formatted version while tracking the original.


---

## Replace List Item

### What it is
Replaces an item at the specified index.

### How it works

This block replaces an item at a specific position in a list with a new value. It uses zero-based indexing and supports negative indices for accessing positions from the end. The old item that was replaced is also returned.

This is useful for updating specific elements in an ordered list without rebuilding the entire list or losing other elements.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| list | The list to modify. | List[Any] | Yes |
| index | Index of the item to replace (supports negative indices). | int | Yes |
| value | The new value for the given index. | Value | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| updated_list | The list after replacement. | List[Any] |
| old_item | The item that was replaced. | Old Item |

### Possible use case

**List Updates**: Replace an outdated entry in a list with updated information while keeping other entries intact.

**Correction Workflows**: Fix a specific item in a results list after validation identifies an error at a known position.

**Value Swapping**: Replace placeholder values in a list with computed or fetched actual values at known positions.


---

## Reverse List Order

### What it is
Reverses the order of elements in a list

### How it works

This block takes a list and returns a new list with all elements in reverse order. The first element becomes the last, and the last element becomes the first. The original list is not modified.

This is useful for changing the processing order of items or displaying lists in a different order than they were collected.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| input_list | The list to reverse | List[Any] | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| reversed_list | The list in reversed order | List[Any] |

### Possible use case

**Chronological Reversal**: Display the most recent items first when data was collected oldest-to-newest.

**Processing Order Change**: Process a stack of items last-in-first-out by reversing a first-in-first-out list.

**Display Formatting**: Reverse leaderboard rankings to show from lowest to highest or vice versa.


---

## Search Memory

### What it is
Search memories in Mem0 by user

### How it works

This block searches through stored memories using a natural language query. It uses semantic search to find memories that are relevant to your query, not just exact matches. Results can be filtered by categories or metadata.

The search is performed against the Mem0 memory store and returns memories ranked by relevance to your query. This enables intelligent recall of past information based on meaning rather than keywords.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| query | Search query | str | Yes |
| trigger | An unused field that is used to (re-)trigger the block when you have no other inputs | bool | No |
| categories_filter | Categories to filter by | List[str] | No |
| metadata_filter | Optional metadata filters to apply | Dict[str, Any] | No |
| limit_memory_to_run | Limit the memory to the run | bool | No |
| limit_memory_to_agent | Limit the memory to the agent | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| memories | List of matching memories | Memories |

### Possible use case

**Contextual Recall**: Search for memories related to a user's current question to provide informed, contextual responses.

**Knowledge Retrieval**: Find previously stored facts or insights that are relevant to a new task or decision.

**Conversation History**: Search past interactions to recall what was discussed about a specific topic or person.


---

## Store Value

### What it is
Holds or receives a value and outputs it statically so that it can be used multiple
times within the same agent run. Whenever consumed, the output value reflects the
last input value that was received.

NOTE: The value is NOT persisted across runs. To store data that survives between
runs, use PersistInformationBlock and RetrieveInformationBlock instead.

### How it works

It accepts an input value and optionally a data value. If a data value is provided, it is used as the output. Otherwise, the input value is used as the output.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| input | Trigger the block to produce the output. The value is only used when `data` is None. | Input | Yes |
| data | The constant data to be retained in the block. This value is passed as `output`. | Data | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| output | The stored data retained in the block. | Output |

### Possible use case

Storing a user's name at the beginning of a workflow to use it in multiple subsequent blocks without asking for it again.


---

## Universal Type Converter

### What it is
This block is used to convert a value to a universal type.

### How it works

This block converts values between common data types: string, number, boolean, list, and dictionary. It handles type coercion intelligently—for example, converting the string "true" to boolean true, or the string "42" to the number 42.

This is useful when data from different sources needs to be in a consistent type for processing, comparison, or API requirements.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| value | The value to convert to a universal type. | Value | Yes |
| type | The type to convert the value to. | "string" \| "number" \| "boolean" \| "list" \| "dictionary" | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| value | The converted value. | Value |

### Possible use case

**API Compatibility**: Convert string inputs to numbers or booleans as required by specific API parameters.

**User Input Processing**: Transform user-entered text values into appropriate types for calculations or logic.

**Data Normalization**: Standardize mixed-type data from various sources into consistent types for processing.


---

## XML Parser

### What it is
Parses XML using gravitasml to tokenize and coverts it to dict

### How it works

This block parses XML content and converts it into a dictionary structure that can be easily navigated and processed in workflows. It uses the gravitasml library to tokenize the XML and produces a nested dictionary matching the XML hierarchy.

This makes XML data accessible using standard dictionary operations, allowing you to extract values, iterate over elements, and process XML-based API responses or data files.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| input_xml | input xml to be parsed | str | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error in parsing | str |
| parsed_xml | output parsed xml to dict | Dict[str, Any] |

### Possible use case

**API Response Processing**: Parse XML responses from SOAP APIs or legacy systems to extract the data you need.

**Configuration File Reading**: Read XML configuration files and convert them to dictionaries for easy access.

**Data Import**: Transform XML data exports from other systems into a format suitable for your workflow processing.


---

## Zip Lists

### What it is
Zips multiple lists together into a list of grouped elements. Supports padding to longest or truncating to shortest.

### How it works

This block pairs up corresponding elements from multiple input lists into sub-lists. For example, zipping `[[1, 2, 3], ['a', 'b', 'c']]` produces `[[1, 'a'], [2, 'b'], [3, 'c']]`.

By default, the result is truncated to the length of the shortest input list. Enable pad_to_longest to instead pad shorter lists with a fill_value so no elements from longer lists are lost.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| lists | A list of lists to zip together. Corresponding elements will be grouped. | List[List[Any]] | Yes |
| pad_to_longest | If True, pad shorter lists with fill_value to match the longest list. If False, truncate to shortest. | bool | No |
| fill_value | Value to use for padding when pad_to_longest is True. | Fill Value | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if zipping failed. | str |
| zipped_list | The zipped list of grouped elements. | List[List[Any]] |
| length | The number of groups in the zipped result. | int |

### Possible use case

**Creating Key-Value Pairs**: Combine a list of field names with a list of values to build structured records or dictionaries.

**Parallel Data Alignment**: Pair up corresponding items from separate data sources (e.g., names and email addresses) for processing together.

**Table Row Construction**: Group column data into rows by zipping each column's values together for CSV export or display.


---

---

### Integrations Block Integrations Branching (`docs/integrations/block-integrations/branching.md`)

# Condition Block

## What it is
The Condition Block is a logical component that evaluates comparisons between two values and produces outputs based on the result.

## What it does
This block compares two input values using a specified comparison operator and determines whether the condition is true or false. It then outputs the result of the comparison and provides corresponding output values for both true and false cases.

## How it works
The block takes two values and a comparison operator as inputs. It then performs the comparison using the specified operator. Based on the result of the comparison, it outputs a boolean value (true or false) and the corresponding output value for the true or false case.

## Inputs
| Input | Description |
|-------|-------------|
| Value 1 | The first value to be compared. This can be any type of value (number, text, or boolean) |
| Operator | The comparison operator to use (e.g., equal to, not equal to, greater than, less than) |
| Value 2 | The second value to be compared. This can be any type of value (number, text, or boolean) |
| Yes Value | (Optional) The value to output if the condition is true. If not provided, Value 1 will be used |
| No Value | (Optional) The value to output if the condition is false. If not provided, Value 1 will be used |

## Outputs
| Output | Description |
|--------|-------------|
| Result | A boolean value (true or false) indicating whether the condition was met |
| Yes Output | The output value if the condition is true. This will be the Yes Value if provided, or Value 1 if not |
| No Output | The output value if the condition is false. This will be the No Value if provided, or Value 1 if not |

## Possible use case
This block could be used in a customer loyalty program to determine if a customer qualifies for a discount. For example, you could compare the customer's total purchases (Value 1) with a threshold amount (Value 2) using the "greater than or equal to" operator. The Yes Value could be "Qualified for discount" and the No Value could be "Not qualified". The block would then output whether the customer qualifies and the appropriate message.

---

### Integrations Block Integrations Claude Code (`docs/integrations/block-integrations/claude_code.md`)

# Claude Code Execution

## What it is
The Claude Code block executes complex coding tasks using Anthropic's Claude Code AI assistant in a secure E2B sandbox environment.

## What it does
This block allows you to delegate coding tasks to Claude Code, which can autonomously create files, install packages, run commands, and build complete applications within a sandboxed environment. Claude Code can handle multi-step development tasks and maintain conversation context across multiple turns.

## How it works
When activated, the block:
1. Creates or connects to an E2B sandbox (a secure, isolated Linux environment)
2. Installs the latest version of Claude Code in the sandbox
3. Optionally runs setup commands to prepare the environment
4. Executes your prompt using Claude Code, which can:
   - Create and edit files
   - Install dependencies (npm, pip, etc.)
   - Run terminal commands
   - Build and test applications
5. Extracts all files created/modified during execution (text files and binary files like images, PDFs, etc.)
6. Returns the response and files, optionally keeping the sandbox alive for follow-up tasks

The block supports conversation continuation through three mechanisms:
- **Same sandbox continuation** (via `session_id` + `sandbox_id`): Resume on the same live sandbox
- **Fresh sandbox continuation** (via `conversation_history`): Restore context on a new sandbox if the previous one timed out
- **Dispose control** (`dispose_sandbox` flag): Keep sandbox alive for multi-turn conversations

## Inputs
| Input | Description |
|-------|-------------|
| E2B Credentials | API key for the E2B platform to create the sandbox. Get one at [e2b.dev](https://e2b.dev/docs) |
| Anthropic Credentials | API key for Anthropic to power Claude Code. Get one at [Anthropic's website](https://console.anthropic.com) |
| Prompt | The task or instruction for Claude Code to execute. Claude Code can create files, install packages, run commands, and perform complex coding tasks |
| Timeout | Sandbox timeout in seconds (default: 300). Set higher for complex tasks. Note: Only applies when creating a new sandbox |
| Setup Commands | Optional shell commands to run before executing Claude Code (e.g., installing dependencies) |
| Working Directory | Working directory for Claude Code to operate in (default: /home/user) |
| Session ID | Session ID to resume a previous conversation. Leave empty for new conversations |
| Sandbox ID | Sandbox ID to reconnect to an existing sandbox. Required when resuming a session |
| Conversation History | Previous conversation history to restore context on a fresh sandbox if the previous one timed out |
| Dispose Sandbox | Whether to dispose of the sandbox after execution (default: true). Set to false to continue conversations later |

## Outputs
| Output | Description |
|--------|-------------|
| Response | The output/response from Claude Code execution |
| Files | List of files (text and binary) created/modified during execution. Includes images, PDFs, and other supported formats. Each file has path, relative_path, name, content, and workspace_ref fields. Binary files are stored in workspace and accessible via workspace_ref |
| Conversation History | Full conversation history including this turn. Use to restore context on a fresh sandbox |
| Session ID | Session ID for this conversation. Pass back with sandbox_id to continue the conversation |
| Sandbox ID | ID of the sandbox instance (null if disposed). Pass back with session_id to continue the conversation |
| Error | Error message if execution failed |

## Possible use case
**API Documentation to Full Application:**
A product team wants to quickly prototype applications based on API documentation. They create an agent that:
1. Uses Firecrawl to fetch API documentation from a URL
2. Passes the docs to Claude Code with a prompt like "Create a web app that demonstrates all the key features of this API"
3. Claude Code builds a complete application with HTML/CSS/JS frontend, proper error handling, and example API calls
4. The Files output is used with GitHub blocks to push the generated code to a new repository

The team can then iterate on the application by passing the sandbox_id and session_id back to Claude Code with refinement requests like "Add authentication" or "Improve the UI", and Claude Code will modify the existing files in the same sandbox.

**Multi-turn Development:**
A developer uses Claude Code to scaffold a new project:
- Turn 1: "Create a Python FastAPI project with user authentication" (dispose_sandbox=false)
- Turn 2: Uses the returned session_id + sandbox_id to ask "Add rate limiting middleware"
- Turn 3: Continues with "Add comprehensive tests"

Each turn builds on the previous work in the same sandbox environment.

---

### Integrations Block Integrations Communication (`docs/integrations/block-integrations/communication.md`)

# Communication

Blocks that send messages from the platform itself, with no account of your own to connect.


## Send Email To Me

### What it is
Email yourself: sends a message to your own inbox, at your account's email address, from the platform, with no email account to sign in to. Use it for "email me" or "send it to myself": a result, a summary or a recurring digest. It can't send to anyone else; to email other people, use Gmail or SMTP.

### How it works

The block hands the subject, body, body format and optional link to the platform's notification service. The body is written in Markdown (the default), plain text or HTML, and all three render into the same AutoGPT email, with a plain-text copy for mail apps that don't show HTML. Markdown and HTML are cleaned first: scripts, forms and inline styles are removed, and links must be web or mailto addresses. Images (`` or an `https://` URL in Markdown, `<img>` in HTML) are fetched once when the email is sent and attached to it, so the mail app never fetches anything. An image that can't be read, isn't a PNG, JPEG, GIF or WebP, or is too big is left out, and its alt text shows instead. The service looks up the email address of the account the workflow or chat is running for, checks that the address is verified and that today's limit (20 emails a day by default) isn't reached, and sends the email through the platform's mail provider. There is no recipient input, so the email can only reach the account owner.

A refusal (an unverified address, the daily limit, or no mail set up on the server) comes back on the `error` output.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| subject | Subject of the email | str | Yes |
| body | Body of the email, written in the chosen format. Images ( or an https:// URL in Markdown, <img> in HTML) are attached to the email. | str | Yes |
| body_format | How the body is written: Markdown, plain text, or HTML (scripts, forms and styles are removed) | "plain" \| "markdown" \| "html" | No |
| link | Optional web link to show as a button, e.g. to the full result | str | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| status | Status of the email sending operation | str |

### Possible use case

**Weekly digest**: a scheduled workflow researches the week's news every Monday and emails you the summary, with no Google account connected.

**Result by email**: you ask Otto to "email me the result", and the finished report lands in your inbox.


---

---

### Integrations Block Integrations Csv (`docs/integrations/block-integrations/csv.md`)

# Read CSV

## What it is
A block that reads and processes CSV (Comma-Separated Values) files.

## What it does
This block takes CSV content as input, processes it, and outputs the data as individual rows and a complete dataset.

## How it works
The Read CSV block takes the contents of a CSV file and splits it into rows and columns. It can handle different formatting options, such as custom delimiters and quote characters. The block processes the CSV data and outputs each row individually, as well as the complete dataset.

## Inputs
| Input | Description |
|-------|-------------|
| Contents | The CSV data as a string |
| Delimiter | The character used to separate values in the CSV (default is comma ",") |
| Quotechar | The character used to enclose fields containing special characters (default is double quote '"') |
| Escapechar | The character used to escape special characters (default is backslash "\") |
| Has_header | Indicates whether the CSV has a header row (default is true) |
| Skip_rows | The number of rows to skip at the beginning of the CSV (default is 0) |
| Strip | Whether to remove leading and trailing whitespace from values (default is true) |
| Skip_columns | A list of column names to exclude from the output (default is an empty list) |

## Outputs
| Output | Description |
|--------|-------------|
| Row | A dictionary representing a single row of the CSV, with column names as keys and cell values as values |
| All_data | A list of dictionaries containing all rows from the CSV |

## Possible use case
This block could be used in a data analysis pipeline to import and process customer information from a CSV file. The individual rows could be used for real-time processing, while the complete dataset could be used for batch analysis or reporting.

---

### Integrations Block Integrations Data (`docs/integrations/block-integrations/data.md`)

# Data

Blocks for creating, reading, and manipulating data structures including lists, dictionaries, spreadsheets, and persistent storage.


## Create Dictionary

### What it is
Creates a dictionary with the specified key-value pairs. Use this when you know all the values you want to add upfront.

### How it works

This block creates a new dictionary from specified key-value pairs in a single operation. It's designed for cases where you know all the data upfront, rather than building the dictionary incrementally.

The block takes a dictionary input and outputs it as-is, making it useful as a starting point for workflows that need to pass structured data between blocks.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| values | Key-value pairs to create the dictionary with | Dict[str, Any] | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if dictionary creation failed | str |
| dictionary | The created dictionary containing the specified key-value pairs | Dict[str, Any] |

### Possible use case

**API Request Payloads**: Create complete request body objects with all required fields before sending to an API.

**Configuration Objects**: Build settings dictionaries with predefined values for initializing services or workflows.

**Data Mapping**: Transform input data into a structured format with specific keys expected by downstream blocks.


---

## Create List

### What it is
Creates a list with the specified values. Use this when you know all the values you want to add upfront. This block can also yield the list in batches based on a maximum size or token limit.

### How it works

This block creates a list from provided values and can optionally chunk it into smaller batches. When max_size is set, the list is yielded in chunks of that size. When max_tokens is set, chunks are sized to fit within token limits for LLM processing.

This batching capability is particularly useful when processing large datasets that need to be split for API limits or memory constraints.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| values | A list of values to be combined into a new list. | List[Any] | Yes |
| max_size | Maximum size of the list. If provided, the list will be yielded in chunks of this size. | int | No |
| max_tokens | Maximum tokens for the list. If provided, the list will be yielded in chunks that fit within this token limit. | int | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| list | The created list containing the specified values. | List[Any] |

### Possible use case

**Batch Processing**: Split large datasets into manageable chunks for API calls with rate limits.

**LLM Token Management**: Divide text content into token-limited batches for processing by language models.

**Parallel Processing**: Create batches of work items that can be processed concurrently by multiple blocks.


---

## File Read

### What it is
Reads a file and returns its content as a string, with optional chunking by delimiter and size limits

### How it works

This block reads file content from various sources (URL, data URI, or local path) and returns it as a string. It supports chunking via delimiter (like newlines) or size limits, yielding content in manageable pieces.

Use skip_rows and skip_size to skip header content or initial bytes. When delimiter and limits are set, content is yielded chunk by chunk, enabling processing of large files without loading everything into memory.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| file_input | The file to read from (URL, data URI, or local path) | str (file) | Yes |
| delimiter | Delimiter to split the content into rows/chunks (e.g., '\n' for lines) | str | No |
| size_limit | Maximum size in bytes per chunk to yield (0 for no limit) | int | No |
| row_limit | Maximum number of rows to process (0 for no limit, requires delimiter) | int | No |
| skip_size | Number of characters to skip from the beginning of the file | int | No |
| skip_rows | Number of rows to skip from the beginning (requires delimiter) | int | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| content | File content, yielded as individual chunks when delimiter or size limits are applied | str |

### Possible use case

**Log File Processing**: Read and process log files line by line, filtering or transforming each entry.

**Large Document Analysis**: Read large text files in chunks for summarization or analysis without memory issues.

**Data Import**: Read text-based data files and process them row by row for database import.


---

## JSON Decoder

### What it is
Decodes a JSON string into the value or data structure, it represents, e.g. an object, list, string, or number.

### How it works

This block uses the project's `orjson`-based decoder to parse a JSON-formatted string and safely convert it into native Python data structures. Valid inputs must strictly follow JSON syntax; for example, passing the string `'{"active": true, "val": null}'` will successfully decode into a Python dictionary where JSON's `true` maps to the Python boolean `True` and `null` maps to `None`. 

If the input string is malformed or contains invalid JSON syntax (such as missing quotes or trailing commas), the internal parser throws an exception. The block catches this exception and raises a `ValueError` that aborts the block execution, integrating with the framework's execution error handling. The legacy schema-level error pin is unused. Edge cases like empty strings or deeply nested structures are handled securely, though extremely deep nesting may be limited by standard parsing recursion depths.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| json_str | The JSON string to decode. | str | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| data | The value as decoded from the JSON string. | Data |

### Possible use case

**API Response Processing**: Parse JSON responses from external APIs into structured data for further processing in your workflow.

**Configuration Loading**: Decode JSON-formatted configuration strings into accessible dictionary settings for your agents.

**Webhook Payload Parsing**: Extract nested fields from incoming JSON webhook payloads for dynamic decision-making.


---

## JSON Encoder

### What it is
Encodes any value or data structure into a JSON string.

### How it works

This block serializes standard Python structures (like `dict`, `list`, `str`, `int`, `float`, `bool`, and `None`) into a valid JSON string using the project's optimized `orjson`-based encoder. It safely handles nested structures, automatically converting Python equivalents to their JSON counterparts (e.g., `{"a": 1}` remains an object, and `None` is translated to `null`).

Before outputting, the block validates JSON-serializability. If an unsupported type is provided—such as custom objects, `datetime`, or `set`s without custom serialization—it raises a `ValueError` that aborts the block execution, integrating with the framework's execution error handling. The legacy schema-level error pin is unused. For edge cases like large numeric precision or non-serializable types, it is recommended to pre-convert these values into strings or dictionaries before passing them to the encoder.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| data | The data structure/value (object, list, string, etc.) to encode into a JSON string. | Data | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| json_str | The JSON string representation of the input data. | str |

### Possible use case

**API Request Formatting**: Convert Python dictionaries into JSON strings for POST/PUT request bodies.

**Data Export**: Serialize structured workflow data into JSON format for saving to files or persistent storage.

**Log Structured Data**: Encode complex data structures into JSON strings for structured logging and debugging output.


---

## Persist Information

### What it is
Persists a key-value pair for use across multiple runs of an agent. Use this when
you need memory that persists between executions, e.g. last-seen state, counters,
accumulated data.

Beware of read->write race conditions for parallel use of the same key.

### How it works

This block stores key-value data that persists across workflow runs. You can scope the persistence to either within_agent (available to all runs of this specific agent) or across_agents (available to all agents for this user).

The stored data remains available until explicitly overwritten, enabling state management and configuration persistence between workflow executions.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| key | Key to store the information under | str | Yes |
| value | Value to store | Value | Yes |
| scope | Scope of persistence: 'within_agent' — shared across all runs of this agent; 'across_agents' — shared across all agents for this user | "within_agent" \| "across_agents" | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| value | Value that was stored | Value |

### Possible use case

**User Preferences**: Store user settings like preferred language or notification preferences for future runs.

**Progress Tracking**: Save the last processed item ID to resume batch processing where you left off.

**API Token Caching**: Store refreshed API tokens that can be reused across multiple workflow executions.


---

## Read Spreadsheet

### What it is
Reads CSV and Excel files and outputs the data as a list of dictionaries and individual rows. Excel files are automatically converted to CSV format.

### How it works

This block parses CSV and Excel files, converting each row into a dictionary with column headers as keys. Excel files are automatically converted to CSV format before processing.

Configure delimiter, quote character, and escape character for proper CSV parsing. Use skip_rows to ignore headers or initial rows, and skip_columns to exclude unwanted columns from the output.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| contents | The contents of the CSV/spreadsheet data to read | str | No |
| file_input | CSV or Excel file to read from (URL, data URI, or local path). Excel files are automatically converted to CSV | str (file) | No |
| sheet_name | Name of the worksheet to read from an Excel file. Defaults to the first sheet. | str | No |
| delimiter | The delimiter used in the CSV/spreadsheet data | str | No |
| quotechar | The character used to quote fields | str | No |
| escapechar | The character used to escape the delimiter | str | No |
| has_header | Whether the CSV file has a header row | bool | No |
| skip_rows | The number of rows to skip from the start of the file | int | No |
| strip | Whether to strip whitespace from the values | bool | No |
| skip_columns | The columns to skip from the start of the row | List[str] | No |
| produce_singular_result | If True, yield individual 'row' outputs only (can be slow). If False, yield both 'rows' (all data) | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| row | The data produced from each row in the spreadsheet | Dict[str, str] |
| rows | All the data in the spreadsheet as a list of rows | List[Dict[str, str]] |

### Possible use case

**Data Import**: Import product catalogs, contact lists, or inventory data from spreadsheet exports.

**Report Processing**: Parse generated CSV reports from other systems for analysis or transformation.

**Bulk Operations**: Process spreadsheets of email addresses, user records, or configuration data row by row.


---

## Retrieve Information

### What it is
Reads back a key-value pair previously saved by PersistInformationBlock.

### How it works

This block retrieves previously stored key-value data for the current user. Specify the key and scope to fetch the corresponding value. If the key doesn't exist, the default_value is returned.

Use within_agent scope for agent-specific data or across_agents for data shared across all user agents.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| key | Key to retrieve the information for | str | Yes |
| scope | Scope of persistence: 'within_agent' — shared across all runs of this agent; 'across_agents' — shared across all agents for this user | "within_agent" \| "across_agents" | No |
| default_value | Default value to return if key is not found | Default Value | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| value | Retrieved value or default value | Value |

### Possible use case

**Resume Processing**: Retrieve the last processed item ID to continue batch operations from where you left off.

**Load Preferences**: Fetch stored user preferences at workflow start to customize behavior.

**State Restoration**: Retrieve workflow state saved from a previous run to maintain continuity.


---

## SQL Query

### What it is
Execute a SQL query. Read-only by default for safety -- disable to allow write operations. Supports PostgreSQL, MySQL, and MSSQL via SQLAlchemy.

### How it works

This block connects to a database using discrete host, port, and database fields and executes a SQL query via SQLAlchemy. It validates that the query is a single statement (using sqlparse to prevent SQL injection via multi-statement attacks), enforces SSRF protections on the database host, and returns results as a list of row dictionaries.

By default, only SELECT queries are allowed (read-only mode). The database session is set to read-only and the transaction is always rolled back. Disable the `read_only` option to allow write operations (INSERT, UPDATE, DELETE, CREATE, DROP, etc.).

Supported database types: PostgreSQL, MySQL, and MSSQL.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| database_type | Database engine | "postgres" \| "mysql" \| "mssql" | No |
| host | Database hostname or IP address. Treated as a secret to avoid leaking infrastructure details. Private/internal IPs are blocked (SSRF protection). | str (password) | Yes |
| port | Database port (leave empty for default: PostgreSQL: 5432, MySQL: 3306, MSSQL: 1433) | int | No |
| database | Name of the database to connect to | str | Yes |
| query | SQL query to execute | str | Yes |
| read_only | When enabled (default), only SELECT queries are allowed and the database session is set to read-only mode. Disable to allow write operations (INSERT, UPDATE, DELETE, etc.). | bool | No |
| timeout | Query timeout in seconds (max 120) | int | No |
| max_rows | Maximum number of rows to return (max 10000) | int | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the query failed | str |
| results | Query results as a list of row dictionaries | List[Dict[str, Any]] |
| columns | Column names from the query result | List[str] |
| row_count | Number of rows returned | int |
| truncated | True when the result set was capped by max_rows, indicating additional rows exist in the database | bool |
| affected_rows | Number of rows affected by a write query (INSERT/UPDATE/DELETE) | int |

### Possible use case

**Analytics Dashboards**: Query your PostgreSQL or MySQL analytics database to pull daily active user counts, revenue metrics, or funnel data directly into your workflow.

**Data Management**: Run INSERT, UPDATE, or DELETE queries to manage data in your databases as part of automated workflows.

**Schema Management**: Create or modify database tables and indexes as part of provisioning or migration workflows.

**Cross-Database Reporting**: Connect to multiple database types (PostgreSQL, MySQL) within a single workflow to aggregate data from different sources.


---

## Screenshot Web Page

### What it is
Takes a screenshot of a specified website using ScreenshotOne API

### How it works

This block uses the ScreenshotOne API to capture screenshots of web pages. Configure viewport dimensions, output format, and whether to capture the full page or just the visible area.

Optional features include blocking ads, cookie banners, and chat widgets for cleaner screenshots. Caching can be enabled to improve performance for repeated captures of the same page.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| url | URL of the website to screenshot | str | Yes |
| viewport_width | Width of the viewport in pixels | int | No |
| viewport_height | Height of the viewport in pixels | int | No |
| full_page | Whether to capture the full page length | bool | No |
| format | Output format (png, jpeg, webp) | "png" \| "jpeg" \| "webp" | No |
| block_ads | Whether to block ads | bool | No |
| block_cookie_banners | Whether to block cookie banners | bool | No |
| block_chats | Whether to block chat widgets | bool | No |
| cache | Whether to enable caching | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| image | The screenshot image data | str (file) |

### Possible use case

**Visual Documentation**: Capture screenshots of web pages for documentation, reports, or archives.

**Competitive Monitoring**: Regularly screenshot competitor websites to track design and content changes.

**Visual Testing**: Capture page renders for visual regression testing or design verification workflows.


---

---

### Integrations Block Integrations Decoder Block (`docs/integrations/block-integrations/decoder_block.md`)

# Text Decoder

## What it is
A tool that converts text with special characters into regular, readable text.

## What it does
It takes a string of text that contains escaped characters (like '\n' for new lines or '\"' for quotation marks) and converts them into their actual representations in the text.

## How it works
The Text Decoder looks at the input text and identifies special character sequences. It then replaces these sequences with their actual characters, making the text more readable and removing any escape characters.

## Inputs
| Input | Description |
|-------|-------------|
| Text | The text you want to decode, which may contain escaped characters like '\n' for new lines or '\"' for quotation marks |

## Outputs
| Output | Description |
|--------|-------------|
| Decoded Text | The text after processing, with all escape sequences converted to their actual characters |
| Error | If there's a problem during the decoding process, an error message will be provided instead |

## Possible use case
Imagine you receive a text message that looks like this: "Hello\nWorld!\nThis is a \"quoted\" string." The Text Decoder can convert it into a more readable format:

```
Hello
World!
This is a "quoted" string.
```

This could be useful when working with data from various sources where text might be encoded to preserve special characters, such as when importing data from a file or receiving it from an API.

---

### Integrations Block Integrations Discord (`docs/integrations/block-integrations/discord.md`)

## Read Discord Messages

### What it is
A block that reads new messages from a Discord channel using a bot token and triggers when a new message is posted.

### What it does
This block connects to Discord using a bot token and retrieves messages from a specified channel. It can operate continuously or retrieve a single message.

### How it works
The block uses a Discord bot to log into a server and listen for new messages. When a message is received, it extracts the content, channel name, and username of the sender. If the message contains a text file attachment, the block also retrieves and includes the file's content.

### Inputs
| Input | Description |
|-------|-------------|
| Discord Bot Token | A secret token used to authenticate the bot with Discord |
| Continuous Read | A boolean flag indicating whether to continuously read messages or stop after one message |

### Outputs
| Output | Description |
|--------|-------------|
| Message Content | The text content of the received message, including any attached file content |
| Channel Name | The name of the Discord channel where the message was received |
| Username | The name of the user who sent the message |

### Possible use case
This block could be used to monitor a Discord channel for support requests. When a user posts a message, the block captures it, allowing another part of the system to process and respond to the request.

---

## Send Discord Message

### What it is
A block that sends messages to a Discord channel using a bot token.

### What it does
This block connects to Discord using a bot token and sends a specified message to a designated channel.

### How it works
The block uses a Discord bot to log into a server, locate the specified channel, and send the provided message. If the message is longer than Discord's character limit, it automatically splits the message into smaller chunks and sends them sequentially.

### Inputs
| Input | Description |
|-------|-------------|
| Discord Bot Token | A secret token used to authenticate the bot with Discord |
| Message Content | The text content of the message to be sent |
| Channel Name | Channel ID or channel name to send the message to |

### Outputs
| Output | Description |
|--------|-------------|
| Status | A string indicating the result of the operation (e.g., "Message sent" or "Channel not found") |

### Possible use case
This block could be used as part of an automated notification system. For example, it could send alerts to a Discord channel when certain events occur in another system, such as when a new user signs up or when a critical error is detected.

---

### Integrations Block Integrations Email Block (`docs/integrations/block-integrations/email_block.md`)

# Send Email

## What it is
The Send Email block is a tool for sending emails using SMTP (Simple Mail Transfer Protocol) credentials.

## What it does
This block allows users to send an email to a specified recipient with a custom subject and body. It uses provided SMTP credentials to connect to an email server and send the message.

## How it works
The block takes the recipient's email address, subject, and body of the email as inputs. It also requires SMTP credentials, including the server address, port, username, and password. The block then connects to the specified SMTP server, authenticates using the provided credentials, and sends the email. After attempting to send the email, it reports back whether the operation was successful or if an error occurred.

## Inputs
| Input | Description |
|-------|-------------|
| To Email | The email address of the recipient |
| Subject | The subject line of the email |
| Body | The main content of the email message |
| SMTP Credentials | Server, port, username, and password for authentication |

### SMTP Credentials Details
| Credential | Description | Default |
|------------|-------------|---------|
| SMTP Server | The address of the SMTP server | smtp.gmail.com |
| SMTP Port | The port number for the SMTP server | 25 |
| SMTP Username | The username for authenticating with the SMTP server | - |
| SMTP Password | The password for authenticating with the SMTP server | - |

## Outputs
| Output | Description |
|--------|-------------|
| Status | A message indicating whether the email was sent successfully |
| Error | If the email sending fails, this output provides details about the error that occurred |

## Possible use case
This block could be used in an automated customer support system. When a customer submits a support ticket through a website, the Send Email block could automatically send a confirmation email to the customer, acknowledging receipt of their request and providing them with a ticket number for future reference.

---

### Integrations Block Integrations Flux Kontext (`docs/integrations/block-integrations/flux_kontext.md`)

# Flux Kontext

## What it is
An internal block that performs text-based image editing using BlackForest Labs' Flux Kontext models.

## What it does
Takes a prompt describing the desired transformation and optionally a reference image, then returns a new image URL.

## How it works
The block sends your prompt, image, and settings to the selected Flux Kontext model on Replicate. The service processes the request and returns a link to the edited image.

## Inputs
| Input        | Description                                                                 |
|--------------|-----------------------------------------------------------------------------|
| Credentials  | Replicate API key with permissions for Flux Kontext models                  |
| Prompt       | Text instruction describing the desired edit                                |
| Input Image  | (Optional) Reference image URI (jpeg, png, gif, webp)                      |
| Aspect Ratio | Aspect ratio of the generated image (e.g. match_input_image, 1:1, 16:9, etc.) |
| Seed         | (Optional, advanced) Random seed for reproducible generation                |
| Model        | Model variant to use: Flux Kontext Pro or Flux Kontext Max                  |

## Outputs
| Output     | Description                              |
|------------|------------------------------------------|
| image_url  | URL of the transformed image             |
| error      | Error message if generation failed       |

## Use Cases
- Enhance a marketing image by requesting "add soft lighting and a subtle vignette" while providing the original asset as the reference image.
- Generate social media assets with specific aspect ratios and style prompts.
- Apply creative edits to product photos using text instructions.

---

### Integrations Block Integrations Google Maps (`docs/integrations/block-integrations/google_maps.md`)

# Google Maps Search

## What it is
A block that searches for local businesses using the Google Maps API.

## What it does
This block allows users to search for places of interest, such as restaurants, shops, or attractions, within a specified area using Google Maps data.

## How it works
The block takes a search query, location details, and API credentials as input. It then communicates with the Google Maps API to fetch information about relevant places. The results are processed and returned as structured data containing details about each place found.

## Inputs
| Input | Description |
|-------|-------------|
| API Key | A secret key required to authenticate and use the Google Maps API |
| Query | The search term for finding local businesses (e.g., "restaurants in New York") |
| Radius | The search area radius in meters, with a maximum of 50,000 meters (about 31 miles) |
| Max Results | The maximum number of places to return, up to 60 results |

## Outputs
| Output | Description |
|--------|-------------|
| Place | Information about a found place, including: |
| - Name | The name of the business or location |
| - Address | The full address of the place |
| - Phone | The contact phone number |
| - Rating | The average rating (out of 5) given by users |
| - Reviews | The total number of user reviews |
| - Website | The official website of the place, if available |
| Error | A message describing any issues that occurred during the search process |

## Possible use case
A travel planning application could use this block to help users discover popular restaurants, attractions, or accommodations in their destination city. By inputting a search query like "family-friendly restaurants in Paris" and specifying a search radius around their hotel, travelers could quickly get a list of suitable dining options with ratings, contact information, and websites for making reservations.

---

### Integrations Block Integrations Http (`docs/integrations/block-integrations/http.md`)

# Send Web Request

## What it is
The Send Web Request block is a tool for making HTTP requests to specified web addresses.

## What it does
This block allows you to send various types of web requests (such as GET, POST, PUT, etc.) to a given URL, optionally including headers and a request body. It then processes the response and categorizes it based on the status code received.

## How it works
When activated, the block takes the provided URL, request method, headers, and body. It then sends the request to the specified web address. Upon receiving a response, it analyzes the status code and returns the response data in one of three categories: successful response, client error, or server error.

## Inputs
| Input | Description |
|-------|-------------|
| URL | The web address to which the request will be sent |
| Method | The type of HTTP request (e.g., GET, POST, PUT). Default is POST |
| Headers | Additional information sent with the request, such as authentication tokens or content type. This is optional |
| Body | The main content of the request, typically used for sending data in POST or PUT requests. This is optional |

## Outputs
| Output | Description |
|--------|-------------|
| Response | The data received from a successful request (status codes 200-299) |
| Client Error | Information about errors caused by the client, such as invalid requests (status codes 400-499) |
| Server Error | Information about errors on the server side (status codes 500-599) |

## Possible use case
This block could be used in an application that needs to interact with external APIs. For example, it could send user data to a registration service, retrieve product information from an e-commerce platform, or post updates to a social media service. The block's ability to handle different types of responses makes it versatile for various web-based interactions.

---

### Integrations Block Integrations Ideogram (`docs/integrations/block-integrations/ideogram.md`)

# Ideogram Model

## What it is
The Ideogram Model block is an AI-powered image generation tool that creates custom images based on text prompts and various settings.

## What it does
This block generates images using the Ideogram AI model, allowing users to create unique visuals by describing what they want in text. It offers various customization options, including different model versions, aspect ratios, and style preferences.

## How it works
The block takes a text prompt and several optional parameters as input. It then sends this information to the Ideogram API, which processes the request and generates an image. The resulting image URL is returned as output. If requested, the block can also upscale the generated image for higher quality.

## Inputs
| Input | Description |
|-------|-------------|
| API Key | Your personal Ideogram API key for authentication |
| Prompt | The text description of the image you want to generate |
| Image Generation Model | Choose from different versions of the Ideogram model |
| Aspect Ratio | Select the desired dimensions for your image |
| Upscale Image | Option to enhance the image quality after generation |
| Magic Prompt Option | Enables automatic enhancement of your text prompt |
| Seed | An optional number for reproducible image generation |
| Style Type | Choose a specific artistic style for your image |
| Negative Prompt | Describe elements you want to exclude from the image |
| Color Palette Preset | Select a predefined color scheme for your image |

## Outputs
| Output | Description |
|--------|-------------|
| Result | The URL of the generated image |
| Error | An error message if something goes wrong during the process |

## Possible use case
A marketing team needs unique visuals for a new product campaign. They can use the Ideogram Model block to quickly generate custom images based on their product descriptions and brand guidelines, exploring different styles and aspect ratios without the need for a professional designer.

---

### Integrations Block Integrations Iteration (`docs/integrations/block-integrations/iteration.md`)

## Step Through Items

### What it is
A block that iterates through a list or dictionary, processing each item one by one.

### What it does
This block takes a list or dictionary as input and goes through each item, outputting the current item and its corresponding key or index.

### How it works
When given a list or dictionary, the block processes each item individually. For lists, it keeps track of the item's position (index). For dictionaries, it focuses on the values, using the value as both the item and the key in the output.

### Inputs
| Input | Description |
|-------|-------------|
| Items | A list or dictionary that you want to process item by item. For example, you could input a list of numbers [1, 2, 3, 4, 5] or a dictionary of key-value pairs {'key1': 'value1', 'key2': 'value2'} |

### Outputs
| Output | Description |
|--------|-------------|
| Item | The current item being processed from the input list or dictionary |
| Key | For lists, this is the index (position) of the current item. For dictionaries, this is the same as the item (the dictionary's value) |

### Possible use case
Imagine you have a list of customer names and you want to perform a specific action for each customer, like sending a personalized email. This block could help you go through the list one by one, allowing you to process each customer individually.

---

### Integrations Block Integrations Llm (`docs/integrations/block-integrations/llm.md`)

# LLM

Blocks for interacting with Large Language Models including AI conversations, image generation, video creation, and intelligent condition evaluation.


## AI Ad Maker Video Creator

### What it is
Creates an AI‑generated 30‑second advert (text + images)

### How it works

This block generates video advertisements by combining AI-generated visuals with narrated scripts. Line breaks in the script create scene transitions. Choose from various voices and background music options.

Optionally provide your own images via input_media_urls, or let the AI generate visuals. The finished video is returned as a URL for download or embedding.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| script | Short advertising copy. Line breaks create new scenes. | str | Yes |
| ratio | Aspect ratio | str | No |
| target_duration | Desired length of the ad in seconds. | int | No |
| voice | Narration voice | "Lily" \| "Daniel" \| "Brian" \| "Jessica" \| "Charlotte" \| "Callum" \| "Eva" | No |
| background_music | Background track | "Observer" \| "Futuristic Beat" \| "Science Documentary" \| "Hotline" \| "Bladerunner 2049" \| "A Future" \| "Elysian Embers" \| "Inspiring Cinematic" \| "Bladerunner Remix" \| "Izzamuzzic" \| "Nas" \| "Paris - Else" \| "Snowfall" \| "Burlesque" \| "Corny Candy" \| "Highway Nocturne" \| "I Don't Think So" \| "Losing Your Marbles" \| "Refresher" \| "Tourist" \| "Twin Tyches" \| "Dont Stop Me Abstract Future Bass" | No |
| input_media_urls | List of image URLs to feature in the advert. | List[str] | No |
| use_only_provided_media | Restrict visuals to supplied images only. | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| video_url | URL of the finished advert | str |

### Possible use case

**Product Marketing**: Create quick promotional videos for products or services.

**Social Media Ads**: Generate short video ads for social media advertising campaigns.

**Content Automation**: Automatically create video ads from product descriptions or scripts.


---

## AI Condition

### What it is
Uses AI to evaluate natural language conditions and provide conditional outputs

### How it works

This block uses an LLM to evaluate natural language conditions that can't be expressed with simple comparisons. Describe the condition in plain English, and the AI determines if it's true or false for the given input.

The result routes data to yes_output or no_output, enabling intelligent branching based on meaning, sentiment, or complex criteria.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| input_value | The input value to evaluate with the AI condition | Input Value | Yes |
| condition | A plaintext English description of the condition to evaluate | str | Yes |
| yes_value | (Optional) Value to output if the condition is true. If not provided, input_value will be used. | Yes Value | No |
| no_value | (Optional) Value to output if the condition is false. If not provided, input_value will be used. | No Value | No |
| model | The language model to use for evaluating the condition. | "o3-mini" \| "o3-2025-04-16" \| "gpt-5.2-2025-12-11" \| "gpt-5.1-2025-11-13" \| "gpt-5-2025-08-07" \| "gpt-5-mini-2025-08-07" \| "o4-mini" \| "o3-pro" \| "o1" \| "o1-mini" \| "gpt-6-astra" \| "gpt-6-sol" \| "gpt-6.1-sol" \| "gpt-6-luna" \| "gpt-5.6-sol" \| "gpt-5.6-terra" \| "gpt-5.6-luna" \| "gpt-5.5-2026-04-23" \| "gpt-5.5-pro" \| "gpt-5.4-2026-03-05" \| "gpt-5.4-mini-2026-03-17" \| "gpt-5.4-nano-2026-03-17" \| "gpt-5.4-pro" \| "gpt-5.3-chat-latest" \| "gpt-5.3-codex" \| "gpt-5.2-pro" \| "gpt-5.1-codex" \| "gpt-5-pro" \| "gpt-4.1-nano" \| "gpt-5-nano-2025-08-07" \| "gpt-5-chat-latest" \| "gpt-4.1-2025-04-14" \| "gpt-4.1-mini-2025-04-14" \| "gpt-4o-mini" \| "gpt-4o" \| "claude-opus-4-5-20251101" \| "claude-sonnet-4-5-20250929" \| "claude-haiku-4-5-20251001" \| "claude-opus-4-6" \| "claude-opus-4-7" \| "claude-opus-4-8" \| "claude-opus-5" \| "claude-opus-5-5" \| "claude-sonnet-4-6" \| "claude-sonnet-5" \| "claude-sonnet-5-5" \| "claude-haiku-5-5" \| "claude-fable-5-1" \| "meta-llama/Llama-3.3-70B-Instruct-Turbo" \| "llama-3.3-70b-versatile" \| "llama-3.1-8b-instant" \| "llama3.3" \| "llama3.2" \| "llama3" \| "llama3.1:405b" \| "dolphin-mistral:latest" \| "openai/gpt-oss-120b" \| "openai/gpt-oss-20b" \| "google/gemini-2.5-pro" \| "google/gemini-3.1-pro-preview" \| "google/gemini-3-flash-preview" \| "google/gemini-2.5-flash" \| "google/gemini-2.0-flash-001" \| "google/gemini-3.1-flash-lite-preview" \| "google/gemini-3.8-flash" \| "google/gemini-4-argon" \| "google/gemini-2.5-flash-lite" \| "google/gemini-2.0-flash-lite-001" \| "google/gemma-4-31b-it" \| "mistralai/mistral-large-2512" \| "mistralai/mistral-large-4-0" \| "mistralai/mistral-medium-3.1" \| "mistralai/mistral-medium-3-5" \| "mistralai/mistral-small-3.2-24b-instruct" \| "mistralai/mistral-small-2603" \| "mistralai/ministral-14b-2512" \| "mistralai/ministral-8b-2512" \| "mistralai/ministral-3b-2512" \| "mistralai/codestral-2508" \| "cohere/command-a-03-2025" \| "cohere/command-a-translate-08-2025" \| "cohere/command-a-reasoning-08-2025" \| "cohere/command-a-vision-07-2025" \| "deepseek/deepseek-chat" \| "deepseek/deepseek-r1-0528" \| "deepseek/deepseek-v4.1-flash" \| "perplexity/sonar" \| "perplexity/sonar-pro" \| "perplexity/sonar-reasoning-pro" \| "perplexity/sonar-deep-research" \| "nousresearch/hermes-3-llama-3.1-405b" \| "nousresearch/hermes-3-llama-3.1-70b" \| "amazon/nova-lite-v1" \| "amazon/nova-micro-v1" \| "amazon/nova-pro-v1" \| "microsoft/phi-4" \| "gryphe/mythomax-l2-13b" \| "inclusionai/ling-3.0-flash-vl" \| "meta-llama/llama-4-scout" \| "meta-llama/llama-4-maverick" \| "meta/muse-spark-1.3" \| "meta/muse-spark-1.3-contributor" \| "meta/muse-spark-open" \| "meta/muse-wonder" \| "x-ai/grok-3" \| "x-ai/grok-4" \| "x-ai/grok-4-fast" \| "x-ai/grok-4.1-fast" \| "x-ai/grok-4.20" \| "x-ai/grok-4.7" \| "x-ai/grok-4.20-multi-agent" \| "x-ai/grok-code-fast-1" \| "moonshotai/kimi-k2.5" \| "moonshotai/kimi-k2.6" \| "moonshotai/kimi-k2-thinking" \| "moonshotai/kimi-k3" \| "qwen/qwen3-235b-a22b-thinking-2507" \| "qwen/qwen3-coder" \| "qwen/qwen3.8-max-0902" \| "qwen/qwen3.8-flash" \| "xiaomi/mimo-v2.6-pro" \| "xiaomi/mimo-v2.6-flash" \| "z-ai/glm-4.6" \| "z-ai/glm-4.6v" \| "z-ai/glm-4.7" \| "z-ai/glm-4.7-flash" \| "z-ai/glm-5" \| "z-ai/glm-5-turbo" \| "z-ai/glm-5v-turbo" \| "z-ai/glm-5.3" \| "sakana/fugu-ultra-v2" \| "inception/mercury-2.5" \| "tencent/hy4-preview" \| "unbiased/pareto" \| "Llama-4-Scout-17B-16E-Instruct-FP8" \| "Llama-4-Maverick-17B-128E-Instruct-FP8" \| "Llama-3.3-8B-Instruct" \| "Llama-3.3-70B-Instruct" \| "v0-1.5-md" \| "v0-1.5-lg" \| "v0-1.0-md" | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the AI evaluation is uncertain or fails | str |
| result | The result of the AI condition evaluation (True or False) | bool |
| yes_output | The output value if the condition is true | Yes Output |
| no_output | The output value if the condition is false | No Output |

### Possible use case

**Sentiment Routing**: Route messages differently based on whether they express frustration or satisfaction.

**Content Moderation**: Check if content contains inappropriate material or policy violations.

**Intent Detection**: Determine if a user message is a question, complaint, or request.


---

## AI Conversation

### What it is
A block that facilitates multi-turn conversations with a Large Language Model (LLM), maintaining context across message exchanges.

### How it works

The block sends the entire conversation history to the chosen LLM, including system messages, user inputs, and previous responses. It then returns the LLM's response as the next part of the conversation.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| prompt | The prompt to send to the language model. | str | No |
| messages | List of messages in the conversation. | List[Any] | Yes |
| model | The language model to use for the conversation. | "o3-mini" \| "o3-2025-04-16" \| "gpt-5.2-2025-12-11" \| "gpt-5.1-2025-11-13" \| "gpt-5-2025-08-07" \| "gpt-5-mini-2025-08-07" \| "o4-mini" \| "o3-pro" \| "o1" \| "o1-mini" \| "gpt-6-astra" \| "gpt-6-sol" \| "gpt-6.1-sol" \| "gpt-6-luna" \| "gpt-5.6-sol" \| "gpt-5.6-terra" \| "gpt-5.6-luna" \| "gpt-5.5-2026-04-23" \| "gpt-5.5-pro" \| "gpt-5.4-2026-03-05" \| "gpt-5.4-mini-2026-03-17" \| "gpt-5.4-nano-2026-03-17" \| "gpt-5.4-pro" \| "gpt-5.3-chat-latest" \| "gpt-5.3-codex" \| "gpt-5.2-pro" \| "gpt-5.1-codex" \| "gpt-5-pro" \| "gpt-4.1-nano" \| "gpt-5-nano-2025-08-07" \| "gpt-5-chat-latest" \| "gpt-4.1-2025-04-14" \| "gpt-4.1-mini-2025-04-14" \| "gpt-4o-mini" \| "gpt-4o" \| "claude-opus-4-5-20251101" \| "claude-sonnet-4-5-20250929" \| "claude-haiku-4-5-20251001" \| "claude-opus-4-6" \| "claude-opus-4-7" \| "claude-opus-4-8" \| "claude-opus-5" \| "claude-opus-5-5" \| "claude-sonnet-4-6" \| "claude-sonnet-5" \| "claude-sonnet-5-5" \| "claude-haiku-5-5" \| "claude-fable-5-1" \| "meta-llama/Llama-3.3-70B-Instruct-Turbo" \| "llama-3.3-70b-versatile" \| "llama-3.1-8b-instant" \| "llama3.3" \| "llama3.2" \| "llama3" \| "llama3.1:405b" \| "dolphin-mistral:latest" \| "openai/gpt-oss-120b" \| "openai/gpt-oss-20b" \| "google/gemini-2.5-pro" \| "google/gemini-3.1-pro-preview" \| "google/gemini-3-flash-preview" \| "google/gemini-2.5-flash" \| "google/gemini-2.0-flash-001" \| "google/gemini-3.1-flash-lite-preview" \| "google/gemini-3.8-flash" \| "google/gemini-4-argon" \| "google/gemini-2.5-flash-lite" \| "google/gemini-2.0-flash-lite-001" \| "google/gemma-4-31b-it" \| "mistralai/mistral-large-2512" \| "mistralai/mistral-large-4-0" \| "mistralai/mistral-medium-3.1" \| "mistralai/mistral-medium-3-5" \| "mistralai/mistral-small-3.2-24b-instruct" \| "mistralai/mistral-small-2603" \| "mistralai/ministral-14b-2512" \| "mistralai/ministral-8b-2512" \| "mistralai/ministral-3b-2512" \| "mistralai/codestral-2508" \| "cohere/command-a-03-2025" \| "cohere/command-a-translate-08-2025" \| "cohere/command-a-reasoning-08-2025" \| "cohere/command-a-vision-07-2025" \| "deepseek/deepseek-chat" \| "deepseek/deepseek-r1-0528" \| "deepseek/deepseek-v4.1-flash" \| "perplexity/sonar" \| "perplexity/sonar-pro" \| "perplexity/sonar-reasoning-pro" \| "perplexity/sonar-deep-research" \| "nousresearch/hermes-3-llama-3.1-405b" \| "nousresearch/hermes-3-llama-3.1-70b" \| "amazon/nova-lite-v1" \| "amazon/nova-micro-v1" \| "amazon/nova-pro-v1" \| "microsoft/phi-4" \| "gryphe/mythomax-l2-13b" \| "inclusionai/ling-3.0-flash-vl" \| "meta-llama/llama-4-scout" \| "meta-llama/llama-4-maverick" \| "meta/muse-spark-1.3" \| "meta/muse-spark-1.3-contributor" \| "meta/muse-spark-open" \| "meta/muse-wonder" \| "x-ai/grok-3" \| "x-ai/grok-4" \| "x-ai/grok-4-fast" \| "x-ai/grok-4.1-fast" \| "x-ai/grok-4.20" \| "x-ai/grok-4.7" \| "x-ai/grok-4.20-multi-agent" \| "x-ai/grok-code-fast-1" \| "moonshotai/kimi-k2.5" \| "moonshotai/kimi-k2.6" \| "moonshotai/kimi-k2-thinking" \| "moonshotai/kimi-k3" \| "qwen/qwen3-235b-a22b-thinking-2507" \| "qwen/qwen3-coder" \| "qwen/qwen3.8-max-0902" \| "qwen/qwen3.8-flash" \| "xiaomi/mimo-v2.6-pro" \| "xiaomi/mimo-v2.6-flash" \| "z-ai/glm-4.6" \| "z-ai/glm-4.6v" \| "z-ai/glm-4.7" \| "z-ai/glm-4.7-flash" \| "z-ai/glm-5" \| "z-ai/glm-5-turbo" \| "z-ai/glm-5v-turbo" \| "z-ai/glm-5.3" \| "sakana/fugu-ultra-v2" \| "inception/mercury-2.5" \| "tencent/hy4-preview" \| "unbiased/pareto" \| "Llama-4-Scout-17B-16E-Instruct-FP8" \| "Llama-4-Maverick-17B-128E-Instruct-FP8" \| "Llama-3.3-8B-Instruct" \| "Llama-3.3-70B-Instruct" \| "v0-1.5-md" \| "v0-1.5-lg" \| "v0-1.0-md" | No |
| max_tokens | The maximum number of tokens to generate in the chat completion. | int | No |
| ollama_host | Ollama host for local  models | str | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| response | The model's response to the conversation. | str |
| prompt | The prompt sent to the language model. | List[Any] |

### Possible use case

Creating an interactive chatbot that can maintain context over multiple exchanges, such as a customer service assistant or a language learning companion.


---

## AI Image Customizer

### What it is
Generate and edit custom images using Google's Nano-Banana models from Gemini. Provide a prompt and optional reference images to create or modify images.

### How it works

This block uses Google's Gemini Nano-Banana models for image generation and editing. Provide a text prompt describing the desired image, and optionally include reference images for style guidance or modification.

Configure aspect ratio to match your needs and choose between JPG or PNG output format. The generated image is returned as a URL.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| prompt | A text description of the image you want to generate | str | Yes |
| model | The AI model to use for image generation and editing | "google/nano-banana" \| "google/nano-banana-pro" \| "google/nano-banana-2" | No |
| images | Optional list of input images to reference or modify | List[str (file)] | No |
| aspect_ratio | Aspect ratio of the generated image | "match_input_image" \| "1:1" \| "2:3" \| "3:2" \| "3:4" \| "4:3" \| "4:5" \| "5:4" \| "9:16" \| "16:9" \| "21:9" | No |
| output_format | Format of the output image | "jpg" \| "png" | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| image_url | URL of the generated image | str (file) |

### Possible use case

**Product Visualization**: Generate product images with different backgrounds or settings.

**Creative Content**: Create unique images for marketing, social media, or presentations.

**Image Modification**: Edit existing images by providing them as references with modification prompts.


---

## AI Image Editor

### What it is
Edit images using Flux Kontext or Google Nano Banana models. Provide a prompt and optional reference image to generate a modified image.

### How it works

This block uses BlackForest Labs' Flux Kontext or Google's Nano Banana models for context-aware image editing. Describe the desired edit in the prompt, and optionally provide an input image to modify.

Choose between Flux Kontext Pro, Max, or Nano Banana models for different quality/speed tradeoffs. Set a seed for reproducible results across multiple runs (Flux Kontext only).


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| prompt | Text instruction describing the desired edit | str | Yes |
| input_image | Reference image URI (jpeg, png, gif, webp) | str (file) | No |
| aspect_ratio | Aspect ratio of the generated image | "match_input_image" \| "1:1" \| "16:9" \| "9:16" \| "4:3" \| "3:4" \| "3:2" \| "2:3" \| "4:5" \| "5:4" \| "21:9" \| "9:21" \| "2:1" \| "1:2" | No |
| seed | Random seed. Set for reproducible generation (Flux Kontext only; ignored by Nano Banana models) | int | No |
| model | Model variant to use | "Flux Kontext Pro" \| "Flux Kontext Max" \| "Nano Banana Pro" \| "Nano Banana 2" | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| output_image | URL of the transformed image | str (file) |

### Possible use case

**Style Transfer**: Transform images to match different artistic styles or moods.

**Object Editing**: Add, remove, or modify specific elements in existing images.

**Background Changes**: Replace or modify image backgrounds while preserving subjects.


---

## AI Image Generator

### What it is
Generate images using various AI models through a unified interface

### How it works

This block generates images from text prompts using your choice of AI models including Flux and Recraft. Select the image size (square, landscape, portrait, wide, or tall) and visual style to match your needs.

The unified interface allows switching between models without changing your workflow, making it easy to compare results or adapt to different use cases.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| prompt | Text prompt for image generation | str | Yes |
| model | The AI model to use for image generation | "Flux 1.1 Pro" \| "Flux 1.1 Pro Ultra" \| "Recraft v3" \| "Stable Diffusion 3.5 Medium" \| "Nano Banana Pro" \| "Nano Banana 2" | No |
| size | Format of the generated image: - Square: Perfect for profile pictures, icons - Landscape: Traditional photo format - Portrait: Vertical photos, portraits - Wide: Cinematic format, desktop wallpapers - Tall: Mobile wallpapers, social media stories | "square" \| "landscape" \| "portrait" \| "wide" \| "tall" | No |
| style | Visual style for the generated image | "any" \| "realistic_image" \| "realistic_image/b_and_w" \| "realistic_image/hdr" \| "realistic_image/natural_light" \| "realistic_image/studio_portrait" \| "realistic_image/enterprise" \| "realistic_image/hard_flash" \| "realistic_image/motion_blur" \| "digital_illustration" \| "digital_illustration/pixel_art" \| "digital_illustration/hand_drawn" \| "digital_illustration/grain" \| "digital_illustration/infantile_sketch" \| "digital_illustration/2d_art_poster" \| "digital_illustration/2d_art_poster_2" \| "digital_illustration/handmade_3d" \| "digital_illustration/hand_drawn_outline" \| "digital_illustration/engraving_color" | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| image_url | URL of the generated image | str |

### Possible use case

**Content Creation**: Generate images for blog posts, articles, or social media.

**Marketing Visuals**: Create product images, banners, or promotional graphics.

**Illustration**: Generate custom illustrations for presentations or documents.


---

## AI List Generator

### What it is
A block that creates lists of items based on prompts using a Large Language Model (LLM), with optional source data for context.

### How it works

The block formulates a prompt based on the given focus or source data, sends it to the chosen LLM, and then processes the response to ensure it's a valid Python list. It can retry multiple times if the initial attempts fail.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| focus | The focus of the list to generate. | str | No |
| source_data | The data to generate the list from. | str | No |
| model | The language model to use for generating the list. | "o3-mini" \| "o3-2025-04-16" \| "gpt-5.2-2025-12-11" \| "gpt-5.1-2025-11-13" \| "gpt-5-2025-08-07" \| "gpt-5-mini-2025-08-07" \| "o4-mini" \| "o3-pro" \| "o1" \| "o1-mini" \| "gpt-6-astra" \| "gpt-6-sol" \| "gpt-6.1-sol" \| "gpt-6-luna" \| "gpt-5.6-sol" \| "gpt-5.6-terra" \| "gpt-5.6-luna" \| "gpt-5.5-2026-04-23" \| "gpt-5.5-pro" \| "gpt-5.4-2026-03-05" \| "gpt-5.4-mini-2026-03-17" \| "gpt-5.4-nano-2026-03-17" \| "gpt-5.4-pro" \| "gpt-5.3-chat-latest" \| "gpt-5.3-codex" \| "gpt-5.2-pro" \| "gpt-5.1-codex" \| "gpt-5-pro" \| "gpt-4.1-nano" \| "gpt-5-nano-2025-08-07" \| "gpt-5-chat-latest" \| "gpt-4.1-2025-04-14" \| "gpt-4.1-mini-2025-04-14" \| "gpt-4o-mini" \| "gpt-4o" \| "claude-opus-4-5-20251101" \| "claude-sonnet-4-5-20250929" \| "claude-haiku-4-5-20251001" \| "claude-opus-4-6" \| "claude-opus-4-7" \| "claude-opus-4-8" \| "claude-opus-5" \| "claude-opus-5-5" \| "claude-sonnet-4-6" \| "claude-sonnet-5" \| "claude-sonnet-5-5" \| "claude-haiku-5-5" \| "claude-fable-5-1" \| "meta-llama/Llama-3.3-70B-Instruct-Turbo" \| "llama-3.3-70b-versatile" \| "llama-3.1-8b-instant" \| "llama3.3" \| "llama3.2" \| "llama3" \| "llama3.1:405b" \| "dolphin-mistral:latest" \| "openai/gpt-oss-120b" \| "openai/gpt-oss-20b" \| "google/gemini-2.5-pro" \| "google/gemini-3.1-pro-preview" \| "google/gemini-3-flash-preview" \| "google/gemini-2.5-flash" \| "google/gemini-2.0-flash-001" \| "google/gemini-3.1-flash-lite-preview" \| "google/gemini-3.8-flash" \| "google/gemini-4-argon" \| "google/gemini-2.5-flash-lite" \| "google/gemini-2.0-flash-lite-001" \| "google/gemma-4-31b-it" \| "mistralai/mistral-large-2512" \| "mistralai/mistral-large-4-0" \| "mistralai/mistral-medium-3.1" \| "mistralai/mistral-medium-3-5" \| "mistralai/mistral-small-3.2-24b-instruct" \| "mistralai/mistral-small-2603" \| "mistralai/ministral-14b-2512" \| "mistralai/ministral-8b-2512" \| "mistralai/ministral-3b-2512" \| "mistralai/codestral-2508" \| "cohere/command-a-03-2025" \| "cohere/command-a-translate-08-2025" \| "cohere/command-a-reasoning-08-2025" \| "cohere/command-a-vision-07-2025" \| "deepseek/deepseek-chat" \| "deepseek/deepseek-r1-0528" \| "deepseek/deepseek-v4.1-flash" \| "perplexity/sonar" \| "perplexity/sonar-pro" \| "perplexity/sonar-reasoning-pro" \| "perplexity/sonar-deep-research" \| "nousresearch/hermes-3-llama-3.1-405b" \| "nousresearch/hermes-3-llama-3.1-70b" \| "amazon/nova-lite-v1" \| "amazon/nova-micro-v1" \| "amazon/nova-pro-v1" \| "microsoft/phi-4" \| "gryphe/mythomax-l2-13b" \| "inclusionai/ling-3.0-flash-vl" \| "meta-llama/llama-4-scout" \| "meta-llama/llama-4-maverick" \| "meta/muse-spark-1.3" \| "meta/muse-spark-1.3-contributor" \| "meta/muse-spark-open" \| "meta/muse-wonder" \| "x-ai/grok-3" \| "x-ai/grok-4" \| "x-ai/grok-4-fast" \| "x-ai/grok-4.1-fast" \| "x-ai/grok-4.20" \| "x-ai/grok-4.7" \| "x-ai/grok-4.20-multi-agent" \| "x-ai/grok-code-fast-1" \| "moonshotai/kimi-k2.5" \| "moonshotai/kimi-k2.6" \| "moonshotai/kimi-k2-thinking" \| "moonshotai/kimi-k3" \| "qwen/qwen3-235b-a22b-thinking-2507" \| "qwen/qwen3-coder" \| "qwen/qwen3.8-max-0902" \| "qwen/qwen3.8-flash" \| "xiaomi/mimo-v2.6-pro" \| "xiaomi/mimo-v2.6-flash" \| "z-ai/glm-4.6" \| "z-ai/glm-4.6v" \| "z-ai/glm-4.7" \| "z-ai/glm-4.7-flash" \| "z-ai/glm-5" \| "z-ai/glm-5-turbo" \| "z-ai/glm-5v-turbo" \| "z-ai/glm-5.3" \| "sakana/fugu-ultra-v2" \| "inception/mercury-2.5" \| "tencent/hy4-preview" \| "unbiased/pareto" \| "Llama-4-Scout-17B-16E-Instruct-FP8" \| "Llama-4-Maverick-17B-128E-Instruct-FP8" \| "Llama-3.3-8B-Instruct" \| "Llama-3.3-70B-Instruct" \| "v0-1.5-md" \| "v0-1.5-lg" \| "v0-1.0-md" | No |
| max_retries | Maximum number of retries for generating a valid list. | int | No |
| force_json_output | Whether to force the LLM to produce a JSON-only response. This can increase the block's reliability, but may also reduce the quality of the response because it prohibits the LLM from reasoning before providing its JSON response. | bool | No |
| max_tokens | The maximum number of tokens to generate in the chat completion. | int | No |
| ollama_host | Ollama host for local  models | str | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| generated_list | The generated list. | List[str] |
| list_item | Each individual item in the list. | str |
| prompt | The prompt sent to the language model. | List[Any] |

### Possible use case

Automatically generating a list of key points or action items from a long meeting transcript or summarizing the main topics discussed in a series of documents.


---

## AI Music Generator

### What it is
This block generates music using Meta's MusicGen model on Replicate.

### How it works

This block uses Meta's MusicGen model to generate original music from text descriptions. Describe the desired music style, mood, and instruments in the prompt, and the AI creates a matching audio track.

Configure duration, temperature (for variety), and output format. Higher temperature produces more diverse results, while lower values stay closer to typical patterns.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| prompt | A description of the music you want to generate | str | Yes |
| music_gen_model_version | Model to use for generation | "stereo-large" \| "melody-large" \| "large" | No |
| duration | Duration of the generated audio in seconds | int | No |
| temperature | Controls the 'conservativeness' of the sampling process. Higher temperature means more diversity | float | No |
| top_k | Reduces sampling to the k most likely tokens | int | No |
| top_p | Reduces sampling to tokens with cumulative probability of p. When set to 0 (default), top_k sampling is used | float | No |
| classifier_free_guidance | Increases the influence of inputs on the output. Higher values produce lower-variance outputs that adhere more closely to inputs | int | No |
| output_format | Output format for generated audio | "wav" \| "mp3" | No |
| normalization_strategy | Strategy for normalizing audio | "loudness" \| "clip" \| "peak" \| "rms" | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| result | URL of the generated audio file | str |

### Possible use case

**Video Soundtracks**: Generate background music for videos, podcasts, or presentations.

**Content Creation**: Create original music for social media or marketing content.

**Prototyping**: Quickly generate music concepts for creative projects.


---

## AI Screenshot To Video Ad

### What it is
Turns a screenshot into an engaging, avatar‑narrated video advert.

### How it works

This block creates video advertisements featuring a screenshot with AI-generated narration. Provide the screenshot URL and narration script, and the block generates a video with voice and background music.

Choose from various voices and music tracks. The video showcases the screenshot while the AI narrator reads your script.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| script | Narration that will accompany the screenshot. | str | Yes |
| screenshot_url | Screenshot or image URL to showcase. | str | Yes |
| ratio | - | str | No |
| target_duration | - | int | No |
| voice | - | "Lily" \| "Daniel" \| "Brian" \| "Jessica" \| "Charlotte" \| "Callum" \| "Eva" | No |
| background_music | - | "Observer" \| "Futuristic Beat" \| "Science Documentary" \| "Hotline" \| "Bladerunner 2049" \| "A Future" \| "Elysian Embers" \| "Inspiring Cinematic" \| "Bladerunner Remix" \| "Izzamuzzic" \| "Nas" \| "Paris - Else" \| "Snowfall" \| "Burlesque" \| "Corny Candy" \| "Highway Nocturne" \| "I Don't Think So" \| "Losing Your Marbles" \| "Refresher" \| "Tourist" \| "Twin Tyches" \| "Dont Stop Me Abstract Future Bass" | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| video_url | Rendered video URL | str |

### Possible use case

**App Demos**: Create narrated demonstrations of software features from screenshots.

**Product Tours**: Turn product screenshots into engaging video walkthroughs.

**Tutorial Videos**: Generate instructional videos from step-by-step screenshots.


---

## AI Shortform Video Creator

### What it is
Creates a shortform video using revid.ai

### How it works

This block creates short-form videos from scripts using revid.ai. Format scripts with line breaks for scene changes and use [brackets] to guide visual generation. Text outside brackets becomes narration.

Choose video style (stock video, moving images, or AI-generated), voice, background music, and generation presets. The finished video URL is returned for download or sharing.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| script | 1. Use short and punctuated sentences  2. Use linebreaks to create a new clip  3. Text outside of brackets is spoken by the AI, and [text between brackets] will be used to guide the visual generation. For example, [close-up of a cat] will show a close-up of a cat. | str | Yes |
| ratio | Aspect ratio of the video | str | No |
| resolution | Resolution of the video | str | No |
| frame_rate | Frame rate of the video | int | No |
| generation_preset | Generation preset for visual style - only affects AI-generated visuals | "Default" \| "Anime" \| "Realist" \| "Illustration" \| "Sketch Color" \| "Sketch B&W" \| "Pixar" \| "Japanese Ink" \| "3D Render" \| "Lego" \| "Sci-Fi" \| "Retro Cartoon" \| "Pixel Art" \| "Creative" \| "Photography" \| "Raytraced" \| "Environment" \| "Fantasy" \| "Anime Realism" \| "Movie" \| "Stylized Illustration" \| "Manga" \| "DEFAULT" | No |
| background_music | Background music track | "Observer" \| "Futuristic Beat" \| "Science Documentary" \| "Hotline" \| "Bladerunner 2049" \| "A Future" \| "Elysian Embers" \| "Inspiring Cinematic" \| "Bladerunner Remix" \| "Izzamuzzic" \| "Nas" \| "Paris - Else" \| "Snowfall" \| "Burlesque" \| "Corny Candy" \| "Highway Nocturne" \| "I Don't Think So" \| "Losing Your Marbles" \| "Refresher" \| "Tourist" \| "Twin Tyches" \| "Dont Stop Me Abstract Future Bass" | No |
| voice | AI voice to use for narration | "Lily" \| "Daniel" \| "Brian" \| "Jessica" \| "Charlotte" \| "Callum" \| "Eva" | No |
| video_style | Type of visual media to use for the video | "stockVideo" \| "movingImage" \| "aiVideo" | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| video_url | The URL of the created video | str |

### Possible use case

**Social Media Content**: Create TikTok, Reels, or Shorts content automatically.

**Explainer Videos**: Generate short educational or promotional videos.

**Content Repurposing**: Convert written content into engaging short-form video.


---

## AI Structured Response Generator

### What it is
A block that generates structured JSON responses using a Large Language Model (LLM), with schema validation and format enforcement.

### How it works

The block sends the input prompt to a chosen LLM, along with any system prompts and expected response format. It then processes the LLM's response, ensuring it matches the expected format, and returns the structured data.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| prompt | The prompt to send to the language model. | str | Yes |
| expected_format | Expected format of the response. If provided, the response will be validated against this format. The keys should be the expected fields in the response, and the values should be the description of the field. | Dict[str, str] | Yes |
| list_result | Whether the response should be a list of objects in the expected format. | bool | No |
| model | The language model to use for answering the prompt. | "o3-mini" \| "o3-2025-04-16" \| "gpt-5.2-2025-12-11" \| "gpt-5.1-2025-11-13" \| "gpt-5-2025-08-07" \| "gpt-5-mini-2025-08-07" \| "o4-mini" \| "o3-pro" \| "o1" \| "o1-mini" \| "gpt-6-astra" \| "gpt-6-sol" \| "gpt-6.1-sol" \| "gpt-6-luna" \| "gpt-5.6-sol" \| "gpt-5.6-terra" \| "gpt-5.6-luna" \| "gpt-5.5-2026-04-23" \| "gpt-5.5-pro" \| "gpt-5.4-2026-03-05" \| "gpt-5.4-mini-2026-03-17" \| "gpt-5.4-nano-2026-03-17" \| "gpt-5.4-pro" \| "gpt-5.3-chat-latest" \| "gpt-5.3-codex" \| "gpt-5.2-pro" \| "gpt-5.1-codex" \| "gpt-5-pro" \| "gpt-4.1-nano" \| "gpt-5-nano-2025-08-07" \| "gpt-5-chat-latest" \| "gpt-4.1-2025-04-14" \| "gpt-4.1-mini-2025-04-14" \| "gpt-4o-mini" \| "gpt-4o" \| "claude-opus-4-5-20251101" \| "claude-sonnet-4-5-20250929" \| "claude-haiku-4-5-20251001" \| "claude-opus-4-6" \| "claude-opus-4-7" \| "claude-opus-4-8" \| "claude-opus-5" \| "claude-opus-5-5" \| "claude-sonnet-4-6" \| "claude-sonnet-5" \| "claude-sonnet-5-5" \| "claude-haiku-5-5" \| "claude-fable-5-1" \| "meta-llama/Llama-3.3-70B-Instruct-Turbo" \| "llama-3.3-70b-versatile" \| "llama-3.1-8b-instant" \| "llama3.3" \| "llama3.2" \| "llama3" \| "llama3.1:405b" \| "dolphin-mistral:latest" \| "openai/gpt-oss-120b" \| "openai/gpt-oss-20b" \| "google/gemini-2.5-pro" \| "google/gemini-3.1-pro-preview" \| "google/gemini-3-flash-preview" \| "google/gemini-2.5-flash" \| "google/gemini-2.0-flash-001" \| "google/gemini-3.1-flash-lite-preview" \| "google/gemini-3.8-flash" \| "google/gemini-4-argon" \| "google/gemini-2.5-flash-lite" \| "google/gemini-2.0-flash-lite-001" \| "google/gemma-4-31b-it" \| "mistralai/mistral-large-2512" \| "mistralai/mistral-large-4-0" \| "mistralai/mistral-medium-3.1" \| "mistralai/mistral-medium-3-5" \| "mistralai/mistral-small-3.2-24b-instruct" \| "mistralai/mistral-small-2603" \| "mistralai/ministral-14b-2512" \| "mistralai/ministral-8b-2512" \| "mistralai/ministral-3b-2512" \| "mistralai/codestral-2508" \| "cohere/command-a-03-2025" \| "cohere/command-a-translate-08-2025" \| "cohere/command-a-reasoning-08-2025" \| "cohere/command-a-vision-07-2025" \| "deepseek/deepseek-chat" \| "deepseek/deepseek-r1-0528" \| "deepseek/deepseek-v4.1-flash" \| "perplexity/sonar" \| "perplexity/sonar-pro" \| "perplexity/sonar-reasoning-pro" \| "perplexity/sonar-deep-research" \| "nousresearch/hermes-3-llama-3.1-405b" \| "nousresearch/hermes-3-llama-3.1-70b" \| "amazon/nova-lite-v1" \| "amazon/nova-micro-v1" \| "amazon/nova-pro-v1" \| "microsoft/phi-4" \| "gryphe/mythomax-l2-13b" \| "inclusionai/ling-3.0-flash-vl" \| "meta-llama/llama-4-scout" \| "meta-llama/llama-4-maverick" \| "meta/muse-spark-1.3" \| "meta/muse-spark-1.3-contributor" \| "meta/muse-spark-open" \| "meta/muse-wonder" \| "x-ai/grok-3" \| "x-ai/grok-4" \| "x-ai/grok-4-fast" \| "x-ai/grok-4.1-fast" \| "x-ai/grok-4.20" \| "x-ai/grok-4.7" \| "x-ai/grok-4.20-multi-agent" \| "x-ai/grok-code-fast-1" \| "moonshotai/kimi-k2.5" \| "moonshotai/kimi-k2.6" \| "moonshotai/kimi-k2-thinking" \| "moonshotai/kimi-k3" \| "qwen/qwen3-235b-a22b-thinking-2507" \| "qwen/qwen3-coder" \| "qwen/qwen3.8-max-0902" \| "qwen/qwen3.8-flash" \| "xiaomi/mimo-v2.6-pro" \| "xiaomi/mimo-v2.6-flash" \| "z-ai/glm-4.6" \| "z-ai/glm-4.6v" \| "z-ai/glm-4.7" \| "z-ai/glm-4.7-flash" \| "z-ai/glm-5" \| "z-ai/glm-5-turbo" \| "z-ai/glm-5v-turbo" \| "z-ai/glm-5.3" \| "sakana/fugu-ultra-v2" \| "inception/mercury-2.5" \| "tencent/hy4-preview" \| "unbiased/pareto" \| "Llama-4-Scout-17B-16E-Instruct-FP8" \| "Llama-4-Maverick-17B-128E-Instruct-FP8" \| "Llama-3.3-8B-Instruct" \| "Llama-3.3-70B-Instruct" \| "v0-1.5-md" \| "v0-1.5-lg" \| "v0-1.0-md" | No |
| force_json_output | Whether to force the LLM to produce a JSON-only response. This can increase the block's reliability, but may also reduce the quality of the response because it prohibits the LLM from reasoning before providing its JSON response. | bool | No |
| sys_prompt | The system prompt to provide additional context to the model. | str | No |
| conversation_history | The conversation history to provide context for the prompt. | List[Dict[str, Any]] | No |
| retry | Number of times to retry the LLM call if the response does not match the expected format. | int | No |
| prompt_values | Values used to fill in the prompt. The values can be used in the prompt by putting them in a double curly braces, e.g. {{variable_name}}. | Dict[str, str] | No |
| max_tokens | The maximum number of tokens to generate in the chat completion. | int | No |
| compress_prompt_to_fit | Whether to compress the prompt to fit within the model's context window. | bool | No |
| ollama_host | Ollama host for local  models | str | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| response | The response object generated by the language model. | Dict[str, Any] \| List[Dict[str, Any]] |
| prompt | The prompt sent to the language model. | List[Any] |

### Possible use case

Extracting specific information from unstructured text, such as generating a product description with predefined fields (name, features, price) from a lengthy product review.


---

## AI Text Generator

### What it is
A block that produces text responses using a Large Language Model (LLM) based on customizable prompts and system instructions.

### How it works

The block sends the input prompt to a chosen LLM, processes the response, and returns the generated text.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| prompt | The prompt to send to the language model. You can use any of the {keys} from Prompt Values to fill in the prompt with values from the prompt values dictionary by putting them in curly braces. | str | Yes |
| model | The language model to use for answering the prompt. | "o3-mini" \| "o3-2025-04-16" \| "gpt-5.2-2025-12-11" \| "gpt-5.1-2025-11-13" \| "gpt-5-2025-08-07" \| "gpt-5-mini-2025-08-07" \| "o4-mini" \| "o3-pro" \| "o1" \| "o1-mini" \| "gpt-6-astra" \| "gpt-6-sol" \| "gpt-6.1-sol" \| "gpt-6-luna" \| "gpt-5.6-sol" \| "gpt-5.6-terra" \| "gpt-5.6-luna" \| "gpt-5.5-2026-04-23" \| "gpt-5.5-pro" \| "gpt-5.4-2026-03-05" \| "gpt-5.4-mini-2026-03-17" \| "gpt-5.4-nano-2026-03-17" \| "gpt-5.4-pro" \| "gpt-5.3-chat-latest" \| "gpt-5.3-codex" \| "gpt-5.2-pro" \| "gpt-5.1-codex" \| "gpt-5-pro" \| "gpt-4.1-nano" \| "gpt-5-nano-2025-08-07" \| "gpt-5-chat-latest" \| "gpt-4.1-2025-04-14" \| "gpt-4.1-mini-2025-04-14" \| "gpt-4o-mini" \| "gpt-4o" \| "claude-opus-4-5-20251101" \| "claude-sonnet-4-5-20250929" \| "claude-haiku-4-5-20251001" \| "claude-opus-4-6" \| "claude-opus-4-7" \| "claude-opus-4-8" \| "claude-opus-5" \| "claude-opus-5-5" \| "claude-sonnet-4-6" \| "claude-sonnet-5" \| "claude-sonnet-5-5" \| "claude-haiku-5-5" \| "claude-fable-5-1" \| "meta-llama/Llama-3.3-70B-Instruct-Turbo" \| "llama-3.3-70b-versatile" \| "llama-3.1-8b-instant" \| "llama3.3" \| "llama3.2" \| "llama3" \| "llama3.1:405b" \| "dolphin-mistral:latest" \| "openai/gpt-oss-120b" \| "openai/gpt-oss-20b" \| "google/gemini-2.5-pro" \| "google/gemini-3.1-pro-preview" \| "google/gemini-3-flash-preview" \| "google/gemini-2.5-flash" \| "google/gemini-2.0-flash-001" \| "google/gemini-3.1-flash-lite-preview" \| "google/gemini-3.8-flash" \| "google/gemini-4-argon" \| "google/gemini-2.5-flash-lite" \| "google/gemini-2.0-flash-lite-001" \| "google/gemma-4-31b-it" \| "mistralai/mistral-large-2512" \| "mistralai/mistral-large-4-0" \| "mistralai/mistral-medium-3.1" \| "mistralai/mistral-medium-3-5" \| "mistralai/mistral-small-3.2-24b-instruct" \| "mistralai/mistral-small-2603" \| "mistralai/ministral-14b-2512" \| "mistralai/ministral-8b-2512" \| "mistralai/ministral-3b-2512" \| "mistralai/codestral-2508" \| "cohere/command-a-03-2025" \| "cohere/command-a-translate-08-2025" \| "cohere/command-a-reasoning-08-2025" \| "cohere/command-a-vision-07-2025" \| "deepseek/deepseek-chat" \| "deepseek/deepseek-r1-0528" \| "deepseek/deepseek-v4.1-flash" \| "perplexity/sonar" \| "perplexity/sonar-pro" \| "perplexity/sonar-reasoning-pro" \| "perplexity/sonar-deep-research" \| "nousresearch/hermes-3-llama-3.1-405b" \| "nousresearch/hermes-3-llama-3.1-70b" \| "amazon/nova-lite-v1" \| "amazon/nova-micro-v1" \| "amazon/nova-pro-v1" \| "microsoft/phi-4" \| "gryphe/mythomax-l2-13b" \| "inclusionai/ling-3.0-flash-vl" \| "meta-llama/llama-4-scout" \| "meta-llama/llama-4-maverick" \| "meta/muse-spark-1.3" \| "meta/muse-spark-1.3-contributor" \| "meta/muse-spark-open" \| "meta/muse-wonder" \| "x-ai/grok-3" \| "x-ai/grok-4" \| "x-ai/grok-4-fast" \| "x-ai/grok-4.1-fast" \| "x-ai/grok-4.20" \| "x-ai/grok-4.7" \| "x-ai/grok-4.20-multi-agent" \| "x-ai/grok-code-fast-1" \| "moonshotai/kimi-k2.5" \| "moonshotai/kimi-k2.6" \| "moonshotai/kimi-k2-thinking" \| "moonshotai/kimi-k3" \| "qwen/qwen3-235b-a22b-thinking-2507" \| "qwen/qwen3-coder" \| "qwen/qwen3.8-max-0902" \| "qwen/qwen3.8-flash" \| "xiaomi/mimo-v2.6-pro" \| "xiaomi/mimo-v2.6-flash" \| "z-ai/glm-4.6" \| "z-ai/glm-4.6v" \| "z-ai/glm-4.7" \| "z-ai/glm-4.7-flash" \| "z-ai/glm-5" \| "z-ai/glm-5-turbo" \| "z-ai/glm-5v-turbo" \| "z-ai/glm-5.3" \| "sakana/fugu-ultra-v2" \| "inception/mercury-2.5" \| "tencent/hy4-preview" \| "unbiased/pareto" \| "Llama-4-Scout-17B-16E-Instruct-FP8" \| "Llama-4-Maverick-17B-128E-Instruct-FP8" \| "Llama-3.3-8B-Instruct" \| "Llama-3.3-70B-Instruct" \| "v0-1.5-md" \| "v0-1.5-lg" \| "v0-1.0-md" | No |
| sys_prompt | The system prompt to provide additional context to the model. | str | No |
| retry | Number of times to retry the LLM call if the response does not match the expected format. | int | No |
| prompt_values | Values used to fill in the prompt. The values can be used in the prompt by putting them in a double curly braces, e.g. {{variable_name}}. | Dict[str, str] | No |
| ollama_host | Ollama host for local  models | str | No |
| max_tokens | The maximum number of tokens to generate in the chat completion. | int | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| response | The response generated by the language model. | str |
| prompt | The prompt sent to the language model. | List[Any] |

### Possible use case

Generating creative writing, such as short stories or poetry, based on a given theme or starting sentence.


---

## AI Text Summarizer

### What it is
A block that summarizes long texts using a Large Language Model (LLM), with configurable focus topics and summary styles.

### How it works

The block splits the input text into smaller chunks, sends each chunk to an LLM for summarization, and then combines these summaries. If the combined summary is still too long, it repeats the process until a concise summary is achieved.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| text | The text to summarize. | str | Yes |
| model | The language model to use for summarizing the text. | "o3-mini" \| "o3-2025-04-16" \| "gpt-5.2-2025-12-11" \| "gpt-5.1-2025-11-13" \| "gpt-5-2025-08-07" \| "gpt-5-mini-2025-08-07" \| "o4-mini" \| "o3-pro" \| "o1" \| "o1-mini" \| "gpt-6-astra" \| "gpt-6-sol" \| "gpt-6.1-sol" \| "gpt-6-luna" \| "gpt-5.6-sol" \| "gpt-5.6-terra" \| "gpt-5.6-luna" \| "gpt-5.5-2026-04-23" \| "gpt-5.5-pro" \| "gpt-5.4-2026-03-05" \| "gpt-5.4-mini-2026-03-17" \| "gpt-5.4-nano-2026-03-17" \| "gpt-5.4-pro" \| "gpt-5.3-chat-latest" \| "gpt-5.3-codex" \| "gpt-5.2-pro" \| "gpt-5.1-codex" \| "gpt-5-pro" \| "gpt-4.1-nano" \| "gpt-5-nano-2025-08-07" \| "gpt-5-chat-latest" \| "gpt-4.1-2025-04-14" \| "gpt-4.1-mini-2025-04-14" \| "gpt-4o-mini" \| "gpt-4o" \| "claude-opus-4-5-20251101" \| "claude-sonnet-4-5-20250929" \| "claude-haiku-4-5-20251001" \| "claude-opus-4-6" \| "claude-opus-4-7" \| "claude-opus-4-8" \| "claude-opus-5" \| "claude-opus-5-5" \| "claude-sonnet-4-6" \| "claude-sonnet-5" \| "claude-sonnet-5-5" \| "claude-haiku-5-5" \| "claude-fable-5-1" \| "meta-llama/Llama-3.3-70B-Instruct-Turbo" \| "llama-3.3-70b-versatile" \| "llama-3.1-8b-instant" \| "llama3.3" \| "llama3.2" \| "llama3" \| "llama3.1:405b" \| "dolphin-mistral:latest" \| "openai/gpt-oss-120b" \| "openai/gpt-oss-20b" \| "google/gemini-2.5-pro" \| "google/gemini-3.1-pro-preview" \| "google/gemini-3-flash-preview" \| "google/gemini-2.5-flash" \| "google/gemini-2.0-flash-001" \| "google/gemini-3.1-flash-lite-preview" \| "google/gemini-3.8-flash" \| "google/gemini-4-argon" \| "google/gemini-2.5-flash-lite" \| "google/gemini-2.0-flash-lite-001" \| "google/gemma-4-31b-it" \| "mistralai/mistral-large-2512" \| "mistralai/mistral-large-4-0" \| "mistralai/mistral-medium-3.1" \| "mistralai/mistral-medium-3-5" \| "mistralai/mistral-small-3.2-24b-instruct" \| "mistralai/mistral-small-2603" \| "mistralai/ministral-14b-2512" \| "mistralai/ministral-8b-2512" \| "mistralai/ministral-3b-2512" \| "mistralai/codestral-2508" \| "cohere/command-a-03-2025" \| "cohere/command-a-translate-08-2025" \| "cohere/command-a-reasoning-08-2025" \| "cohere/command-a-vision-07-2025" \| "deepseek/deepseek-chat" \| "deepseek/deepseek-r1-0528" \| "deepseek/deepseek-v4.1-flash" \| "perplexity/sonar" \| "perplexity/sonar-pro" \| "perplexity/sonar-reasoning-pro" \| "perplexity/sonar-deep-research" \| "nousresearch/hermes-3-llama-3.1-405b" \| "nousresearch/hermes-3-llama-3.1-70b" \| "amazon/nova-lite-v1" \| "amazon/nova-micro-v1" \| "amazon/nova-pro-v1" \| "microsoft/phi-4" \| "gryphe/mythomax-l2-13b" \| "inclusionai/ling-3.0-flash-vl" \| "meta-llama/llama-4-scout" \| "meta-llama/llama-4-maverick" \| "meta/muse-spark-1.3" \| "meta/muse-spark-1.3-contributor" \| "meta/muse-spark-open" \| "meta/muse-wonder" \| "x-ai/grok-3" \| "x-ai/grok-4" \| "x-ai/grok-4-fast" \| "x-ai/grok-4.1-fast" \| "x-ai/grok-4.20" \| "x-ai/grok-4.7" \| "x-ai/grok-4.20-multi-agent" \| "x-ai/grok-code-fast-1" \| "moonshotai/kimi-k2.5" \| "moonshotai/kimi-k2.6" \| "moonshotai/kimi-k2-thinking" \| "moonshotai/kimi-k3" \| "qwen/qwen3-235b-a22b-thinking-2507" \| "qwen/qwen3-coder" \| "qwen/qwen3.8-max-0902" \| "qwen/qwen3.8-flash" \| "xiaomi/mimo-v2.6-pro" \| "xiaomi/mimo-v2.6-flash" \| "z-ai/glm-4.6" \| "z-ai/glm-4.6v" \| "z-ai/glm-4.7" \| "z-ai/glm-4.7-flash" \| "z-ai/glm-5" \| "z-ai/glm-5-turbo" \| "z-ai/glm-5v-turbo" \| "z-ai/glm-5.3" \| "sakana/fugu-ultra-v2" \| "inception/mercury-2.5" \| "tencent/hy4-preview" \| "unbiased/pareto" \| "Llama-4-Scout-17B-16E-Instruct-FP8" \| "Llama-4-Maverick-17B-128E-Instruct-FP8" \| "Llama-3.3-8B-Instruct" \| "Llama-3.3-70B-Instruct" \| "v0-1.5-md" \| "v0-1.5-lg" \| "v0-1.0-md" | No |
| focus | The topic to focus on in the summary | str | No |
| style | The style of the summary to generate. | "concise" \| "detailed" \| "bullet points" \| "numbered list" | No |
| max_tokens | The maximum number of tokens to generate in the chat completion. | int | No |
| chunk_overlap | The number of overlapping tokens between chunks to maintain context. | int | No |
| ollama_host | Ollama host for local  models | str | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| summary | The final summary of the text. | str |
| prompt | The prompt sent to the language model. | List[Any] |

### Possible use case

Summarizing lengthy research papers or articles to quickly grasp the main points and key findings.


---

## Claude Code

### What it is
Execute tasks using Claude Code in an E2B sandbox. Claude Code can create files, install tools, run commands, and perform complex coding tasks autonomously.

### How it works

When activated, the block:
1. Creates or connects to an E2B sandbox (a secure, isolated Linux environment)
2. Installs the latest version of Claude Code in the sandbox
3. Optionally runs setup commands to prepare the environment
4. Executes your prompt using Claude Code, which can create/edit files, install dependencies, run terminal commands, and build applications
5. Extracts all files created/modified during execution (text files and binary files like images, PDFs, etc.)
6. Returns the response and files, optionally keeping the sandbox alive for follow-up tasks

The block supports conversation continuation through three mechanisms:
- **Same sandbox continuation** (via `session_id` + `sandbox_id`): Resume on the same live sandbox
- **Fresh sandbox continuation** (via `conversation_history`): Restore context on a new sandbox if the previous one timed out
- **Dispose control** (`dispose_sandbox` flag): Keep sandbox alive for multi-turn conversations


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| prompt | The task or instruction for Claude Code to execute. Claude Code can create files, install packages, run commands, and perform complex coding tasks. | str | No |
| timeout | Sandbox timeout in seconds. Claude Code tasks can take a while, so set this appropriately for your task complexity. Note: This only applies when creating a new sandbox. When reconnecting to an existing sandbox via sandbox_id, the original timeout is retained. | int | No |
| setup_commands | Optional shell commands to run before executing Claude Code. Useful for installing dependencies or setting up the environment. | List[str] | No |
| working_directory | Working directory for Claude Code to operate in. | str | No |
| session_id | Session ID to resume a previous conversation. Leave empty for a new conversation. Use the session_id from a previous run to continue that conversation. | str | No |
| sandbox_id | Sandbox ID to reconnect to an existing sandbox. Required when resuming a session (along with session_id). Use the sandbox_id from a previous run where dispose_sandbox was False. | str | No |
| conversation_history | Previous conversation history to continue from. Use this to restore context on a fresh sandbox if the previous one timed out. Pass the conversation_history output from a previous run. | str | No |
| dispose_sandbox | Whether to dispose of the sandbox immediately after execution. Set to False if you want to continue the conversation later (you'll need both sandbox_id and session_id from the output). | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if execution failed | str |
| response | The output/response from Claude Code execution | str |
| files | List of files created/modified by Claude Code during this execution. Includes text files and binary files (images, PDFs, etc.). Each file has 'path', 'relative_path', 'name', 'content', and 'workspace_ref' fields. workspace_ref contains a workspace:// URI for workspace storage. For binary files, content contains a placeholder; use workspace_ref to access the file. | List[SandboxFileOutput] |
| conversation_history | Full conversation history including this turn. Pass this to conversation_history input to continue on a fresh sandbox if the previous sandbox timed out. | str |
| session_id | Session ID for this conversation. Pass this back along with sandbox_id to continue the conversation. | str |
| sandbox_id | ID of the sandbox instance. Pass this back along with session_id to continue the conversation. This is None if dispose_sandbox was True (sandbox was disposed). | str |

### Possible use case

**API Documentation to Full Application**: A product team wants to quickly prototype applications based on API documentation. They fetch API docs with Firecrawl, pass them to Claude Code with a prompt like "Create a web app that demonstrates all the key features of this API", and Claude Code builds a complete application with HTML/CSS/JS frontend, proper error handling, and example API calls. The Files output can then be pushed to GitHub.

**Multi-turn Development**: A developer uses Claude Code to scaffold a new project iteratively - Turn 1: "Create a Python FastAPI project with user authentication" (dispose_sandbox=false), Turn 2: Uses the returned session_id + sandbox_id to ask "Add rate limiting middleware", Turn 3: Continues with "Add comprehensive tests". Each turn builds on the previous work in the same sandbox environment.

**Automated Code Review and Fixes**: An agent receives code from a PR, sends it to Claude Code with "Review this code for bugs and security issues, then fix any problems you find", and Claude Code analyzes the code, makes fixes, and returns the corrected files ready to commit.


---

## Code Generation

### What it is
Generate or refactor code using an OpenAI API key or a connected ChatGPT plan through Codex App Server.

### How it works

Choose `openai_api` to call the selected Codex model through the OpenAI Responses API with an OpenAI API key. This path applies `model`, `max_output_tokens` (up to 128,000), and every reasoning effort except `ultra`; `none` omits the reasoning configuration. It rejects non-OpenAI API-key credentials and unsupported `ultra` requests before making an API call.

Choose `codex_app_server` to use a connected ChatGPT plan. This path requires matching Codex OAuth credentials and an active credential lease, selects the subscription model from the live App Server catalog, and uses the plan's output-token limit. The `model` and `max_output_tokens` inputs therefore do not affect App Server requests, while all listed reasoning efforts are forwarded through the App Server mapping. Missing or mismatched credentials fail validation, and transport failures are returned through the block's standard `error` output. Successful responses include the generated text, any reasoning summary, and the transport response ID; usage may be absent without failing the response.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| prompt | Primary coding request passed to the Codex model. | str | Yes |
| system_prompt | Optional instructions passed to the selected Codex transport. | str | No |
| transport | Use an OpenAI API key, or your connected ChatGPT subscription through Codex App Server if supported by your plan. | "openai_api" \| "codex_app_server" | No |
| model | OpenAI API transport only. Codex App Server selects the current subscription model from its live model catalog. | "gpt-5.6-sol" \| "gpt-5.6-terra" \| "gpt-5.6-luna" \| "gpt-5.3-codex" \| "gpt-5.1-codex" | No |
| reasoning_effort | Controls the selected transport's reasoning effort. OpenAI API does not support 'ultra'; select 'none' to omit reasoning config. | "none" \| "low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" | No |
| max_output_tokens | OpenAI API transport only: upper bound for generated tokens (hard limit 128,000). Codex App Server uses its model and plan limits. | int | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| response | Code-focused response returned by the Codex model. | str |
| reasoning | Reasoning summary returned by the model, if available. | str |
| response_id | Transport response ID for auditing and debugging. | str |

### Possible use case

**Code Automation**: Generate boilerplate code, functions, or entire modules from descriptions.

**Refactoring**: Transform existing code to follow different patterns or conventions.

**Code Completion**: Fill in missing implementation details based on signatures or comments.


---

## Create Talking Avatar Video

### What it is
This block integrates with D-ID to create video clips and retrieve their URLs.

### How it works

The block sends a request to the D-ID API with your specified parameters. It then regularly checks the status of the video creation process until it's complete or an error occurs.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| script_input | The text input for the script | str | Yes |
| provider | The voice provider to use | "microsoft" \| "elevenlabs" \| "amazon" | No |
| voice_id | The voice ID to use, see [available voice IDs](https://agpt.co/docs/platform/using-ai-services/d_id) | str | No |
| presenter_id | The presenter ID to use | str | No |
| driver_id | The driver ID to use | str | No |
| result_format | The desired result format | "mp4" \| "gif" \| "wav" | No |
| crop_type | The crop type for the presenter | "wide" \| "square" \| "vertical" | No |
| subtitles | Whether to include subtitles | bool | No |
| ssml | Whether the input is SSML | bool | No |
| max_polling_attempts | Maximum number of polling attempts | int | No |
| polling_interval | Interval between polling attempts in seconds | int | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| video_url | The URL of the created video | str |

### Possible use case

A marketing team could use this block to create engaging video content for social media. They could input a script promoting a new product, select a friendly-looking avatar, and generate a video that explains the product's features in an appealing way.


---

## Ideogram Model

### What it is
This block runs Ideogram models with both simple and advanced settings.

### How it works

This block generates images using Ideogram's models (V1, V2, V3) which excel at rendering text within images. Configure aspect ratio, style type, and optionally enable MagicPrompt for enhanced results.

Advanced options include upscaling, custom color palettes, and negative prompts to exclude unwanted elements. Set a seed for reproducible generation.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| prompt | Text prompt for image generation | str | Yes |
| ideogram_model_name | The name of the Image Generation Model, e.g., V_3 | "V_3" \| "V_2" \| "V_1" \| "V_1_TURBO" \| "V_2_TURBO" | No |
| aspect_ratio | Aspect ratio for the generated image | "ASPECT_10_16" \| "ASPECT_16_10" \| "ASPECT_9_16" \| "ASPECT_16_9" \| "ASPECT_3_2" \| "ASPECT_2_3" \| "ASPECT_4_3" \| "ASPECT_3_4" \| "ASPECT_1_1" \| "ASPECT_1_3" \| "ASPECT_3_1" | No |
| upscale | Upscale the generated image | "AI Upscale" \| "No Upscale" | No |
| magic_prompt_option | Whether to use MagicPrompt for enhancing the request | "AUTO" \| "ON" \| "OFF" | No |
| seed | Random seed. Set for reproducible generation | int | No |
| style_type | Style type to apply, applicable for V_2 and above | "AUTO" \| "GENERAL" \| "REALISTIC" \| "DESIGN" \| "RENDER_3D" \| "ANIME" | No |
| negative_prompt | Description of what to exclude from the image | str | No |
| color_palette_name | Color palette preset name, choose 'None' to skip | "NONE" \| "EMBER" \| "FRESH" \| "JUNGLE" \| "MAGIC" \| "MELON" \| "MOSAIC" \| "PASTEL" \| "ULTRAMARINE" | No |
| custom_color_palette | Only available for model version V_2 or V_2_TURBO. Provide one or more color hex codes (e.g., ['#000030', '#1C0C47', '#9900FF', '#4285F4', '#FFFFFF']) to define a custom color palette. Only used if 'color_palette_name' is 'NONE'. | List[str] | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| result | Generated image URL | str |

### Possible use case

**Text-Heavy Graphics**: Create images with logos, signs, or text overlays.

**Marketing Materials**: Generate promotional images with clear, readable text.

**Social Media Graphics**: Create quote images, announcements, or branded content with text.


---

## Orchestrator

### What it is
Uses AI to intelligently decide what tool to use.

### How it works

This block enables agentic behavior by letting an LLM decide which tools to use based on the prompt. It can execute connected tools and feed their results back to the model, creating autonomous reasoning loops.

In the default `built_in` execution mode, configure `agent_mode_max_iterations` to control loop behavior: `0` makes one decision and yields tool calls for external execution, `-1` loops until finished, and a positive number caps the iterations. The block outputs tool calls or a finished message. The `extended_thinking` mode manages its own loop and ignores this iteration setting.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| prompt | The prompt to send to the language model. | str | Yes |
| model | The language model to use for answering the prompt. | "o3-mini" \| "o3-2025-04-16" \| "gpt-5.2-2025-12-11" \| "gpt-5.1-2025-11-13" \| "gpt-5-2025-08-07" \| "gpt-5-mini-2025-08-07" \| "o4-mini" \| "o3-pro" \| "o1" \| "o1-mini" \| "gpt-6-astra" \| "gpt-6-sol" \| "gpt-6.1-sol" \| "gpt-6-luna" \| "gpt-5.6-sol" \| "gpt-5.6-terra" \| "gpt-5.6-luna" \| "gpt-5.5-2026-04-23" \| "gpt-5.5-pro" \| "gpt-5.4-2026-03-05" \| "gpt-5.4-mini-2026-03-17" \| "gpt-5.4-nano-2026-03-17" \| "gpt-5.4-pro" \| "gpt-5.3-chat-latest" \| "gpt-5.3-codex" \| "gpt-5.2-pro" \| "gpt-5.1-codex" \| "gpt-5-pro" \| "gpt-4.1-nano" \| "gpt-5-nano-2025-08-07" \| "gpt-5-chat-latest" \| "gpt-4.1-2025-04-14" \| "gpt-4.1-mini-2025-04-14" \| "gpt-4o-mini" \| "gpt-4o" \| "claude-opus-4-5-20251101" \| "claude-sonnet-4-5-20250929" \| "claude-haiku-4-5-20251001" \| "claude-opus-4-6" \| "claude-opus-4-7" \| "claude-opus-4-8" \| "claude-opus-5" \| "claude-opus-5-5" \| "claude-sonnet-4-6" \| "claude-sonnet-5" \| "claude-sonnet-5-5" \| "claude-haiku-5-5" \| "claude-fable-5-1" \| "meta-llama/Llama-3.3-70B-Instruct-Turbo" \| "llama-3.3-70b-versatile" \| "llama-3.1-8b-instant" \| "llama3.3" \| "llama3.2" \| "llama3" \| "llama3.1:405b" \| "dolphin-mistral:latest" \| "openai/gpt-oss-120b" \| "openai/gpt-oss-20b" \| "google/gemini-2.5-pro" \| "google/gemini-3.1-pro-preview" \| "google/gemini-3-flash-preview" \| "google/gemini-2.5-flash" \| "google/gemini-2.0-flash-001" \| "google/gemini-3.1-flash-lite-preview" \| "google/gemini-3.8-flash" \| "google/gemini-4-argon" \| "google/gemini-2.5-flash-lite" \| "google/gemini-2.0-flash-lite-001" \| "google/gemma-4-31b-it" \| "mistralai/mistral-large-2512" \| "mistralai/mistral-large-4-0" \| "mistralai/mistral-medium-3.1" \| "mistralai/mistral-medium-3-5" \| "mistralai/mistral-small-3.2-24b-instruct" \| "mistralai/mistral-small-2603" \| "mistralai/ministral-14b-2512" \| "mistralai/ministral-8b-2512" \| "mistralai/ministral-3b-2512" \| "mistralai/codestral-2508" \| "cohere/command-a-03-2025" \| "cohere/command-a-translate-08-2025" \| "cohere/command-a-reasoning-08-2025" \| "cohere/command-a-vision-07-2025" \| "deepseek/deepseek-chat" \| "deepseek/deepseek-r1-0528" \| "deepseek/deepseek-v4.1-flash" \| "perplexity/sonar" \| "perplexity/sonar-pro" \| "perplexity/sonar-reasoning-pro" \| "perplexity/sonar-deep-research" \| "nousresearch/hermes-3-llama-3.1-405b" \| "nousresearch/hermes-3-llama-3.1-70b" \| "amazon/nova-lite-v1" \| "amazon/nova-micro-v1" \| "amazon/nova-pro-v1" \| "microsoft/phi-4" \| "gryphe/mythomax-l2-13b" \| "inclusionai/ling-3.0-flash-vl" \| "meta-llama/llama-4-scout" \| "meta-llama/llama-4-maverick" \| "meta/muse-spark-1.3" \| "meta/muse-spark-1.3-contributor" \| "meta/muse-spark-open" \| "meta/muse-wonder" \| "x-ai/grok-3" \| "x-ai/grok-4" \| "x-ai/grok-4-fast" \| "x-ai/grok-4.1-fast" \| "x-ai/grok-4.20" \| "x-ai/grok-4.7" \| "x-ai/grok-4.20-multi-agent" \| "x-ai/grok-code-fast-1" \| "moonshotai/kimi-k2.5" \| "moonshotai/kimi-k2.6" \| "moonshotai/kimi-k2-thinking" \| "moonshotai/kimi-k3" \| "qwen/qwen3-235b-a22b-thinking-2507" \| "qwen/qwen3-coder" \| "qwen/qwen3.8-max-0902" \| "qwen/qwen3.8-flash" \| "xiaomi/mimo-v2.6-pro" \| "xiaomi/mimo-v2.6-flash" \| "z-ai/glm-4.6" \| "z-ai/glm-4.6v" \| "z-ai/glm-4.7" \| "z-ai/glm-4.7-flash" \| "z-ai/glm-5" \| "z-ai/glm-5-turbo" \| "z-ai/glm-5v-turbo" \| "z-ai/glm-5.3" \| "sakana/fugu-ultra-v2" \| "inception/mercury-2.5" \| "tencent/hy4-preview" \| "unbiased/pareto" \| "Llama-4-Scout-17B-16E-Instruct-FP8" \| "Llama-4-Maverick-17B-128E-Instruct-FP8" \| "Llama-3.3-8B-Instruct" \| "Llama-3.3-70B-Instruct" \| "v0-1.5-md" \| "v0-1.5-lg" \| "v0-1.0-md" | No |
| multiple_tool_calls | Whether to allow multiple tool calls in a single response. | bool | No |
| sys_prompt | The system prompt to provide additional context to the model. | str | No |
| conversation_history | The conversation history to provide context for the prompt. | List[Dict[str, Any]] | No |
| last_tool_output | The output of the last tool that was called. | Last Tool Output | No |
| retry | Number of times to retry the LLM call if the response does not match the expected format. | int | No |
| prompt_values | Values used to fill in the prompt. The values can be used in the prompt by putting them in a double curly braces, e.g. {{variable_name}}. | Dict[str, str] | No |
| max_tokens | The maximum number of tokens to generate in the chat completion. | int | No |
| ollama_host | Ollama host for local  models | str | No |
| agent_mode_max_iterations | Maximum iterations for agent mode. 0 = traditional mode (single LLM call, yield tool calls for external execution), -1 = infinite agent mode (loop until finished), 1+ = agent mode with max iterations limit. | int | No |
| execution_mode | How tool calls are executed. 'built_in' uses the default tool-call loop (all providers). 'extended_thinking' delegates to an external Agent SDK for richer reasoning (currently Anthropic / OpenRouter only, requires API credentials, ignores 'Agent Mode Max Iterations'). | "built_in" \| "extended_thinking" | No |
| conversation_compaction | Automatically compact the context window once it hits the limit | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| tools | The tools that are available to use. | Tools |
| finished | The finished message to display to the user. | str |
| conversations | The conversation history to provide context for the prompt. | List[Any] |

### Possible use case

**Autonomous Agents**: Build agents that can independently decide which tools to use for a task.

**Dynamic Workflows**: Create workflows that adapt their execution path based on model decisions.

**Multi-Tool Orchestration**: Let a model coordinate multiple tools to accomplish complex goals.


---

## Perplexity

### What it is
Query Perplexity's sonar models with real-time web search capabilities and receive annotated responses with source citations.

### How it works

This block queries Perplexity's sonar models which combine LLM capabilities with real-time web search. Responses include source citations as annotations, providing verifiable references for the information.

Choose from different sonar model variants including deep-research for comprehensive analysis. The block returns both the response text and structured citation data.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| prompt | The query to send to the Perplexity model. | str | Yes |
| model | The Perplexity sonar model to use. | "perplexity/sonar" \| "perplexity/sonar-pro" \| "perplexity/sonar-deep-research" | No |
| system_prompt | Optional system prompt to provide context to the model. | str | No |
| max_tokens | The maximum number of tokens to generate. | int | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| response | The response from the Perplexity model. | str |
| annotations | List of URL citations and annotations from the response. | List[Dict[str, Any]] |

### Possible use case

**Research Automation**: Get answers to questions with verifiable sources for fact-checking.

**Current Events**: Query real-time information that LLMs with static training data can't provide.

**Competitive Intelligence**: Research companies, products, or markets with cited sources.


---

## Unreal Text To Speech

### What it is
Converts text to speech using the Unreal Speech API

### How it works

This block converts text into natural-sounding speech using Unreal Speech API. Provide the text content and optionally select a specific voice ID to customize the audio output.

The generated audio is returned as an MP3 URL that can be downloaded, played, or used as input for video creation blocks.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| text | The text to be converted to speech | str | Yes |
| voice_id | The voice ID to use for text-to-speech conversion | str | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| mp3_url | The URL of the generated MP3 file | str |

### Possible use case

**Voiceover Generation**: Create narration audio for videos, presentations, or tutorials.

**Accessibility**: Convert written content to audio for visually impaired users.

**Audio Content**: Generate podcast intros, announcements, or automated phone messages.


---

---

### Integrations Block Integrations Logic (`docs/integrations/block-integrations/logic.md`)

# Logic

Blocks for conditional logic, comparisons, calculations, data sampling, and control flow operations in workflows.


## Calculator

### What it is
Performs a mathematical operation on two numbers.

### How it works

The Calculator block takes in two numbers and an operation choice. It then applies the chosen operation to the numbers and returns the result. If rounding is selected, it rounds the result to the nearest whole number.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| operation | Choose the math operation you want to perform | "Add" \| "Subtract" \| "Multiply" \| "Divide" \| "Power" | Yes |
| a | Enter the first number (A) | float | Yes |
| b | Enter the second number (B) | float | Yes |
| round_result | Do you want to round the result to a whole number? | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| result | The result of your calculation | float |

### Possible use case

A user wants to quickly perform a calculation, such as adding two numbers or calculating a percentage. They can input the numbers and operation into this block and receive the result instantly.


---

## Condition

### What it is
Handles conditional logic based on comparison operators

### How it works

This block compares two values using standard operators (==, !=, >, <, >=, <=) and routes data based on the result. The comparison result determines which output receives data: yes_output for true conditions, no_output for false.

Optionally specify yes_value and no_value to output different data than the input values. If not specified, value1 is used as the output value.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| value1 | Enter the first value for comparison | Value1 | Yes |
| operator | Choose the comparison operator | "==" \| "!=" \| ">" \| "<" \| ">=" \| "<=" | Yes |
| value2 | Enter the second value for comparison | Value2 | Yes |
| yes_value | (Optional) Value to output if the condition is true. If not provided, value1 will be used. | Yes Value | No |
| no_value | (Optional) Value to output if the condition is false. If not provided, value1 will be used. | No Value | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| result | The result of the condition evaluation (True or False) | bool |
| yes_output | The output value if the condition is true | Yes Output |
| no_output | The output value if the condition is false | No Output |

### Possible use case

**Threshold Checks**: Route workflow differently when values exceed limits (e.g., order total > $100 triggers approval).

**Status Validation**: Check if a status equals "complete" or "error" to branch workflow logic.

**Numeric Comparisons**: Compare scores, counts, or metrics to conditionally trigger actions.


---

## Count Items

### What it is
Counts the number of items in a collection.

### How it works

The Count Items block receives a collection as input. It then determines the type of collection and uses the appropriate method to count the items. For most collections, it uses the length function. For other iterable objects, it counts the items one by one.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| collection | Enter the collection you want to count. This can be a list, dictionary, string, or any other iterable. | Collection | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| count | The number of items in the collection | int |

### Possible use case

A user has a list of customer names and wants to quickly determine how many customers are in the list. They can input the list into this block and receive the total count immediately.


---

## Data Sampling

### What it is
This block samples data from a given dataset using various sampling methods.

### How it works

This block extracts a sample from a dataset using various methods: random sampling, systematic sampling (every nth item), or top-N selection. Advanced options include stratified sampling by key, weighted sampling, and cluster sampling.

Configure sample_size to control how many items to select. Use random_seed for reproducible results. The accumulate option collects data before sampling, useful when processing streaming inputs.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| data | The dataset to sample from. Can be a single dictionary, a list of dictionaries, or a list of lists. | Dict[str, Any] \| List[Dict[str, Any] \| List[Any]] | Yes |
| sample_size | The number of samples to take from the dataset. | int | No |
| sampling_method | The method to use for sampling. | "random" \| "systematic" \| "top" \| "bottom" \| "stratified" \| "weighted" \| "reservoir" \| "cluster" | No |
| accumulate | Whether to accumulate data before sampling. | bool | No |
| random_seed | Seed for random number generator (optional). | int | No |
| stratify_key | Key to use for stratified sampling (required for stratified sampling). | str | No |
| weight_key | Key to use for weighted sampling (required for weighted sampling). | str | No |
| cluster_key | Key to use for cluster sampling (required for cluster sampling). | str | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| sampled_data | The sampled subset of the input data. | List[Dict[str, Any] \| List[Any]] |
| sample_indices | The indices of the sampled data in the original dataset. | List[int] |

### Possible use case

**A/B Testing**: Randomly sample users or records for testing different workflow paths.

**Representative Subsets**: Extract stratified samples from large datasets for analysis or testing.

**Performance Testing**: Select a smaller sample from large data for workflow development and debugging.


---

## If Input Matches

### What it is
Handles conditional logic based on comparison operators

### How it works

This block checks if an input matches a specified value and routes data accordingly. When the input equals the value, data flows to yes_output; otherwise, it goes to no_output.

Use yes_value and no_value to specify what data to output in each case. This provides a simple equality check for branching workflow logic.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| input | The input to match against | Input | Yes |
| value | The value to output if the input matches | Value | Yes |
| yes_value | The value to output if the input matches | Yes Value | No |
| no_value | The value to output if the input does not match | No Value | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| result | The result of the condition evaluation (True or False) | bool |
| yes_output | The output value if the condition is true | Yes Output |
| no_output | The output value if the condition is false | No Output |

### Possible use case

**Category Routing**: Route items to different processing paths based on their category or type.

**Feature Flags**: Check if a feature flag equals "enabled" to conditionally execute new functionality.

**Status Handling**: Branch workflow based on specific status values like "pending", "approved", or "rejected".


---

## Pinecone Init

### What it is
Initializes a Pinecone index

### How it works

This block initializes or connects to a Pinecone vector database index. Specify the index name, vector dimension, and distance metric (cosine, euclidean, or dot product) for new indexes.

For serverless deployment, configure the cloud provider and region. The block returns the initialized index name for use with insert and query operations.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| index_name | Name of the Pinecone index | str | Yes |
| dimension | Dimension of the vectors | int | No |
| metric | Distance metric for the index | str | No |
| cloud | Cloud provider for serverless | str | No |
| region | Region for serverless | str | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| index | Name of the initialized Pinecone index | str |
| message | Status message | str |

### Possible use case

**RAG Pipeline Setup**: Initialize a vector index for storing document embeddings in retrieval-augmented generation.

**Semantic Search**: Set up a vector database for similarity search across products, documents, or media.

**Knowledge Base**: Create a searchable vector store for FAQ answers or support documentation.


---

## Pinecone Insert

### What it is
Upload data to a Pinecone index

### How it works

This block uploads vectors and associated text chunks to a Pinecone index. Each chunk is paired with its embedding vector, and optional metadata can be attached for filtering during queries.

Use namespaces to organize vectors into logical groups within the same index. The upsert operation adds new vectors or updates existing ones with matching IDs.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| index | Initialized Pinecone index | str | Yes |
| chunks | List of text chunks to ingest | List[Any] | Yes |
| embeddings | List of embeddings corresponding to the chunks | List[Any] | Yes |
| namespace | Namespace to use in Pinecone | str | No |
| metadata | Additional metadata to store with each vector | Dict[str, Any] | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| upsert_response | Response from Pinecone upsert operation | str |

### Possible use case

**Document Indexing**: Store document chunks with their embeddings for later semantic search.

**Knowledge Ingestion**: Add FAQ entries, product descriptions, or support articles to a searchable index.

**Memory Storage**: Store conversation history or agent memories as searchable vectors.


---

## Pinecone Query

### What it is
Queries a Pinecone index

### How it works

This block searches a Pinecone index for vectors similar to a query vector. Specify top_k to control how many results to return, and use namespace to search within a specific partition.

Results include similarity scores and optionally the vector values and metadata. Combined results aggregate the text chunks for easy use in RAG pipelines.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| query_vector | Query vector | List[Any] | Yes |
| namespace | Namespace to query in Pinecone | str | No |
| top_k | Number of top results to return | int | No |
| include_values | Whether to include vector values in the response | bool | No |
| include_metadata | Whether to include metadata in the response | bool | No |
| host | Host for pinecone | str | No |
| idx_name | Index name for pinecone | str | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| results | Query results from Pinecone | Results |
| combined_results | Combined results from Pinecone | Combined Results |

### Possible use case

**Semantic Search**: Find the most relevant documents or answers based on meaning, not just keywords.

**RAG Context**: Retrieve relevant context passages to augment LLM prompts with domain-specific knowledge.

**Similar Content**: Find products, articles, or media similar to a reference item.


---

## Step Through Items

### What it is
Iterates over a list or dictionary and outputs each item.

### How it works

When given a list or dictionary, the block processes each item individually. For lists, it keeps track of the item's position (index). For dictionaries, it focuses on the values, using the value as both the item and the key in the output.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| items | The list or dictionary of items to iterate over | List[Any] | No |
| items_object | The list or dictionary of items to iterate over | Dict[str, Any] | No |
| items_str | The list or dictionary of items to iterate over | str | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| item | The current item in the iteration | Item |
| key | The key or index of the current item in the iteration | Key |

### Possible use case

Imagine you have a list of customer names and you want to perform a specific action for each customer, like sending a personalized email. This block could help you go through the list one by one, allowing you to process each customer individually.


---

---

### Integrations Block Integrations Maths (`docs/integrations/block-integrations/maths.md`)

# Mathematical Operations Blocks

## Calculator

### What it is
A block that performs mathematical operations on two numbers.

### What it does
This block takes two numbers and performs a selected mathematical operation (addition, subtraction, multiplication, division, or exponentiation) on them. It can also optionally round the result to a whole number.

### How it works
The Calculator block takes in two numbers and an operation choice. It then applies the chosen operation to the numbers and returns the result. If rounding is selected, it rounds the result to the nearest whole number.

### Inputs
| Input | Description |
|-------|-------------|
| Operation | Choose the math operation you want to perform (Add, Subtract, Multiply, Divide, or Power) |
| A | Enter the first number for the calculation |
| B | Enter the second number for the calculation |
| Round result | Choose whether to round the result to a whole number (True or False) |

### Outputs
| Output | Description |
|--------|-------------|
| Result | The result of your calculation |

### Possible use case
A user wants to quickly perform a calculation, such as adding two numbers or calculating a percentage. They can input the numbers and operation into this block and receive the result instantly.

---

## Count Items

### What it is
A block that counts the number of items in a collection.

### What it does
This block takes a collection (such as a list, dictionary, or string) and counts the number of items within it.

### How it works
The Count Items block receives a collection as input. It then determines the type of collection and uses the appropriate method to count the items. For most collections, it uses the length function. For other iterable objects, it counts the items one by one.

### Inputs
| Input | Description |
|-------|-------------|
| Collection | Enter the collection you want to count (e.g., a list, dictionary, or string) |

### Outputs
| Output | Description |
|--------|-------------|
| Count | The number of items in the collection |

### Possible use case
A user has a list of customer names and wants to quickly determine how many customers are in the list. They can input the list into this block and receive the total count immediately.

---

### Integrations Block Integrations Medium (`docs/integrations/block-integrations/medium.md`)

# Publish to Medium

## What it is
The Publish to Medium block is a tool that enables direct publication of content to the Medium platform from within an automated workflow.

## What it does
This block takes a fully formatted blog post, along with associated metadata, and publishes it to Medium using the platform's API. It handles all aspects of the publication process, including setting the title, content, tags, and other post-specific details.

## How it works
The block uses the provided Medium API key and author ID to authenticate with the Medium platform. It then constructs an API request containing all the post details and sends it to Medium's servers. After the post is published, the block retrieves and returns relevant information about the newly created post, such as its unique ID and public URL.

## Inputs
| Input | Description |
|-------|-------------|
| Author ID | The unique identifier for the Medium author account |
| Title | The headline of the Medium post |
| Content | The main body of the post (in HTML or Markdown format) |
| Content Format | Specifies whether the content is in 'html' or 'markdown' format |
| Tags | Up to 5 topic tags to categorize the post (comma-separated) |
| Canonical URL | The original URL if the content was first published elsewhere |
| Publish Status | Sets the post visibility: 'public', 'draft', or 'unlisted' |
| License | The copyright license for the post (default: 'all-rights-reserved') |
| Notify Followers | Boolean flag to notify the author's followers about the new post |
| API Key | The Medium API key for authentication |

## Outputs
| Output | Description |
|--------|-------------|
| Post ID | The unique identifier assigned to the published post by Medium |
| Post URL | The public web address where the post can be viewed |
| Published At | The timestamp indicating when the post was published |
| Error | Any error message returned if the publication process fails |

## Possible use case
A digital marketing team could integrate this block into their content management system to streamline their cross-platform publishing strategy. After creating and approving a blog post in their main system, they could use this block to automatically publish the content to Medium, ensuring consistent and timely distribution across multiple platforms without manual intervention.

---

### Integrations Block Integrations Misc (`docs/integrations/block-integrations/misc.md`)

# Misc

Miscellaneous blocks including agent execution, scheduling, HTTP requests, webhooks, and other utility functions.


## Agent Executor

### What it is
Executes an existing agent inside your agent

### How it works

This block runs another agent as a sub-agent within your workflow. You provide the agent's graph ID, version, and input data, and the block executes that agent and returns its outputs.

Input and output schemas define the expected data structure for communication between the parent and child agents, enabling modular, reusable agent composition.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| user_id | User ID | str | Yes |
| graph_id | Graph ID | str | Yes |
| graph_version | Graph Version | int | Yes |
| agent_name | Name to display in the Builder UI | str | No |
| inputs | Input data for the graph | Dict[str, Any] | Yes |
| input_schema | Input schema for the graph | Dict[str, Any] | Yes |
| output_schema | Output schema for the graph | Dict[str, Any] | Yes |

### Possible use case

**Modular Workflows**: Break complex workflows into smaller, reusable agents that can be composed together.

**Specialized Agents**: Call domain-specific agents (like a research agent or formatter) from a main orchestration agent.

**Dynamic Routing**: Execute different agents based on input type or user preferences.


---

## Approve Reddit Post

### What it is
Approves a Reddit post or comment from the mod queue. Requires 'modposts' scope. Reddit scopes are account-wide, so this grants the ability across every subreddit you moderate, not only the one set here.

### How it works

This block requires a prefixed Reddit thing ID — `t3_...` for a post or `t1_...` for a comment — and loads the matching PRAW object before calling the moderator `approve()` action with credentials that include the `modposts` scope. Bare IDs are rejected: posts and comments share an ID namespace, so an unprefixed ID could resolve to an unrelated object.

On success, the block returns the original `post_id` and `success=True`. `Mod Queue` emits prefixed IDs on its `post_id` output, so the two blocks chain directly without losing whether the item started as a comment or a submission.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| post_id | Post or comment to approve. Full Reddit thing ID, prefixed with 't3_' for a post (e.g. 't3_abc123') or 't1_' for a comment (e.g. 't1_xyz789'). Bare IDs are rejected: posts and comments share an ID namespace, so an unprefixed ID cannot be resolved safely. | str | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| post_id | ID of the approved post (pass-through) | str |
| success | Whether the approval succeeded | bool |

### Possible use case

**False Positive Cleanup**: Approve items from the mod queue after an external classifier determines they are safe.

**Appeal Handling**: Restore a previously filtered post or comment after a moderator reviews a user appeal.

**Hybrid Moderation**: Combine automated queue scoring with a final approval step for borderline content.


---

## AutoPilot

### What it is
Execute tasks using AutoGPT AutoPilot with full access to platform tools (agent management, workspace files, web fetch, block execution, and more). Enables sub-agent patterns and scheduled autopilot execution.

### How it works

This block invokes the platform's copilot system directly via `stream_chat_completion_sdk`. It creates (or resumes) a chat session, streams the autopilot's response collecting text deltas, tool call details, and token usage, then returns the aggregated results. A recursion depth guard prevents infinite loops when the autopilot calls this block as a sub-agent.

Tool and block identifiers provided in `tools` and `blocks` are validated at run time before any execution begins — unknown names or UUIDs produce an error output immediately. When valid, the permissions object is passed into the SDK layer, which narrows the `allowed_tools` list sent to Claude; blocks are enforced at the `run_block` tool call site so the copilot cannot circumvent the filter by calling `run_block` directly. For sub-agent patterns (where an autopilot invokes another AutoPilot block), permissions are inherited: the child's effective allowed set is intersected with the parent's, so a sub-agent can only be *more* restrictive than its parent, never more permissive.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| prompt | The task or instruction for the autopilot to execute. The autopilot has access to platform tools like agent management, workspace files, web fetch, block execution, and more. | str | Yes |
| system_context | Optional additional context prepended to the prompt. Use this to constrain autopilot behavior, provide domain context, or set output format requirements. | str | No |
| transport | Run on platform credits, or on your connected ChatGPT subscription if supported by your plan. | "platform" \| "codex_app_server" | No |
| session_id | Session ID to continue an existing autopilot conversation. Leave empty to start a new session. Use the session_id output from a previous run to continue. | str | No |
| max_recursion_depth | Maximum nesting depth when the autopilot calls this block recursively (sub-agent pattern). Prevents infinite loops. | int | No |
| tools | Tool names to filter. Works with tools_exclude to form an allow-list or deny-list. Leave empty to apply no tool filter. | List["add_understanding" \| "ask_question" \| "bash_exec" \| "browser_act" \| "browser_navigate" \| "browser_screenshot" \| "confirm_expert_change" \| "confirm_expert_soul_update" \| "connect_integration" \| "consult_teammate" \| "create_agent" \| "create_feature_request" \| "create_folder" \| "customize_agent" \| "decompose_goal" \| "delegate_to_expert" \| "delete_folder" \| "delete_preset" \| "delete_schedule" \| "delete_skill" \| "delete_workspace_file" \| "describe_capability" \| "edit_agent" \| "edit_chat_platform_message" \| "enter_agent_building_mode" \| "expert_onboarding" \| "find_agent" \| "find_capability" \| "find_library_agent" \| "find_session" \| "fix_agent_graph" \| "get_agent_building_guide" \| "get_doc_page" \| "get_platform_info" \| "get_sub_session_result" \| "grant_expert_credential" \| "handoff_to_expert" \| "hire_expert" \| "install_expert_workflow" \| "list_agent_triggers" \| "list_chat_platform_channels" \| "list_expert_chats" \| "list_expert_credentials" \| "list_expert_workflows" \| "list_folders" \| "list_presets" \| "list_routines" \| "list_schedules" \| "list_skills" \| "list_team" \| "list_workspace_files" \| "memory_forget_confirm" \| "memory_forget_search" \| "memory_search" \| "memory_store" \| "message_session" \| "move_agents_to_folder" \| "move_folder" \| "pause_schedule" \| "post_to_chat_platform" \| "raise_expert" \| "read_expert_chat" \| "read_skill" \| "read_workspace_file" \| "remove_expert_workflow" \| "request_credential_grant" \| "resume_capability" \| "resume_schedule" \| "revoke_expert_credential" \| "run_agent" \| "run_capability" \| "run_sub_session" \| "schedule_followup" \| "schedule_routine" \| "search_docs" \| "search_feature_requests" \| "setup_agent_webhook_trigger" \| "start_desktop" \| "store_skill" \| "update_expert" \| "update_expert_soul" \| "update_folder" \| "update_preset" \| "validate_agent_graph" \| "view_agent_output" \| "web_fetch" \| "web_search" \| "write_workspace_file" \| "run_block" \| "run_mcp_tool" \| "Agent" \| "Edit" \| "Glob" \| "Grep" \| "Read" \| "Task" \| "TodoWrite" \| "WebSearch" \| "Write"] | No |
| tools_exclude | Controls how the 'tools' list is interpreted. True (default): 'tools' is a deny-list — listed tools are blocked, all others are allowed. An empty 'tools' list means allow everything. False: 'tools' is an allow-list — only listed tools are permitted. | bool | No |
| blocks | Block identifiers to filter when the copilot runs blocks via run_capability. Each entry can be: a block name (e.g. 'HTTP Request'), a full block UUID, or the first 8 hex characters of the UUID (e.g. 'c069dc6b'). Works with blocks_exclude. Leave empty to apply no block filter. | List[str] | No |
| blocks_exclude | Controls how the 'blocks' list is interpreted. True (default): 'blocks' is a deny-list — listed blocks are blocked, all others are allowed. An empty 'blocks' list means allow everything. False: 'blocks' is an allow-list — only listed blocks are permitted. | bool | No |
| providers | Connected-account providers (e.g. 'github') whose accounts the run may use: from code in the sandbox, and through blocks that act with that provider's credentials. Works with providers_exclude. Leave empty to apply no provider filter. | List[str] | No |
| providers_exclude | Controls how the 'providers' list is interpreted. True (default): 'providers' is a deny-list — listed providers are blocked, all others are allowed. An empty 'providers' list means allow everything. False: 'providers' is an allow-list — of the supported connected-account providers, only listed ones are permitted. Other providers are not covered. | bool | No |
| dry_run | When enabled, run_capability and run_agent tool calls in this autopilot session are forced to use dry-run simulation mode. No real API calls, side effects, or credits are consumed by those tools. Useful for testing agent wiring and previewing outputs. Only applies when creating a new session (session_id is empty). When reusing an existing session_id, the session's original dry_run setting is preserved. | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| response | The final text response from the autopilot. | str |
| tool_calls | List of tools called during execution. Each entry has tool_call_id, tool_name, input, output, and success fields. | List[ToolCallEntry] |
| conversation_history | Current turn messages (user prompt + assistant reply) as JSON. It can be used for logging or analysis. | str |
| session_id | Session ID for this conversation. Pass this back to continue the conversation in a future run. | str |
| token_usage | Token usage statistics: prompt_tokens, completion_tokens, total_tokens. | TokenUsage |

### Possible use case

**Scheduled Reports**: Schedule an autopilot to run daily that checks workspace files, summarizes recent agent activity, and posts a report.

**Multi-Step AI Workflows**: Chain autopilot blocks where one gathers data and another analyzes it, enabling complex AI pipelines within the graph editor.

**Sub-Agent Delegation**: Delegate a research or formatting task to a sub-autopilot while the parent agent handles orchestration.


---

## Ban Subreddit User

### What it is
Bans a user from a subreddit. Requires 'modcontributors' scope. Reddit scopes are account-wide, so this grants the ability across every subreddit you moderate, not only the one set here.

### How it works

The input schema bounds the optional `duration` to 1–999 days (Reddit's cap for temporary bans) and caps the internal `reason`, `mod_note`, and user-facing `ban_message` fields at Reddit's moderation limits, so invalid values are rejected before any API call is made. The block then calls `sub.banned.add(...)`, optionally including a temporary duration and a `ban_message`.

The outputs echo the target user and subreddit, plus `success` and a derived `permanent` flag so downstream steps can branch on temporary versus permanent bans. Use `reason` and `mod_note` for internal moderation context, and reserve `ban_message` for the explanation shown to the banned user.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| subreddit | Subreddit to ban the user from, excluding the /r/ prefix | str | Yes |
| username | Reddit username to ban (without the u/ prefix) | str | Yes |
| duration | Ban duration in days (1-999, Reddit's cap for temporary bans). Leave blank for a permanent ban. | int | No |
| reason | Internal moderator-only ban reason (max 100 chars). Use ban_message to explain the ban to the user. | str | No |
| mod_note | Internal moderator note (not shown to the user) | str | No |
| ban_message | Optional custom message sent to the user explaining the ban | str | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| username | Banned username (pass-through) | str |
| subreddit | Subreddit (pass-through) | str |
| success | Whether the ban was applied | bool |
| permanent | True if the ban is permanent | bool |

### Possible use case

**Escalation Workflow**: Ban repeat offenders automatically after multiple confirmed moderation violations.

**Temporary Cooldown**: Apply short bans during heated incidents while moderators review the situation.

**Policy Enforcement**: Pair user reporting or detection blocks with standardized ban actions and messaging.


---

## Create Reddit Post

### What it is
Create a new post on a subreddit. Can create text posts or link posts.

### How it works

This block uses the Reddit API via PRAW to create a new post in the specified subreddit. Provide the title and either text content for a self-post or a URL for a link post. Optionally apply flair using a flair ID from the GetSubredditFlairsBlock.

The block returns the created post's ID and URL, which can be used for chaining with comment blocks or monitoring.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| subreddit | Subreddit to post to, excluding the /r/ prefix | str | Yes |
| title | Title of the post | str | Yes |
| content | Body text of the post (for text posts) | str | No |
| url | URL to submit (for link posts). If provided, content is ignored. | str | No |
| flair_id | Flair template ID to apply to the post (from GetSubredditFlairsBlock) | str | No |
| flair_text | Custom flair text (only used if the flair template allows editing) | str | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| post_id | ID of the created post | str |
| post_url | URL of the created post | str |
| subreddit | The subreddit name (pass-through for chaining) | str |

### Possible use case

**Content Distribution**: Automatically share articles or content to relevant subreddits.

**Community Engagement**: Post updates or announcements to subreddit communities.

**Automated Posting**: Schedule and post content to Reddit based on workflow triggers.


---

## Delete Reddit Comment

### What it is
Delete a Reddit comment that you own.

### How it works

This block uses the Reddit API via PRAW to delete a comment you previously posted. The deletion is permanent and removes the comment from the post thread. You can only delete your own comments.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| comment_id | The ID of the comment to delete (must be your own comment) | str | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if deletion failed | str |
| success | Whether the deletion was successful | bool |
| comment_id | The comment ID (pass-through for chaining) | str |

### Possible use case

**Content Cleanup**: Remove outdated or incorrect comments from discussions.

**Automated Moderation**: Delete comments that fail quality checks or receive negative feedback.


---

## Delete Reddit Post

### What it is
Delete a Reddit post that you own.

### How it works

This block uses the Reddit API via PRAW to delete a post you previously created. The deletion is permanent and removes the post from the subreddit. You can only delete your own posts.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| post_id | The ID of the post to delete (must be your own post) | str | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if deletion failed | str |
| success | Whether the deletion was successful | bool |
| post_id | The post ID (pass-through for chaining) | str |

### Possible use case

**Content Management**: Remove posts that are no longer relevant or contain errors.

**Automated Cleanup**: Delete posts based on performance metrics or time-based rules.


---

## Edit Reddit Post

### What it is
Edit the body text of an existing Reddit post that you own. Only works for self/text posts.

### How it works

This block uses the Reddit API via PRAW to edit the body text of a self-post you created. Link posts cannot be edited. The new content replaces the existing post body.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| post_id | The ID of the post to edit (must be your own post) | str | Yes |
| new_content | The new body text for the post | str | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the edit failed | str |
| success | Whether the edit was successful | bool |
| post_id | The post ID (pass-through for chaining) | str |
| post_url | URL of the edited post | str |

### Possible use case

**Content Updates**: Update posts with new information or corrections.

**Dynamic Content**: Modify post content based on changing data or feedback.


---

## Execute Code

### What it is
Executes code in a sandbox environment with internet access. Python and JavaScript code can take up to 10 MB of input data through `variables`.

### How it works

This block executes Python, JavaScript, or Bash code in an isolated E2B sandbox with internet access. Use setup_commands to install dependencies before running your code.

The sandbox includes pip and npm pre-installed. Set timeout to limit execution time, and use dispose_sandbox to clean up after execution or keep the sandbox running for follow-up steps.

**Passing data in with `variables` (Python and JavaScript):** each key becomes a variable of the same name in your code, and its value arrives exactly as given, whatever quotes, backslashes or unicode it holds. The limit is 10 MB of JSON in total:

- Up to 64 KB, the data is also in the `AGPT_VARIABLES` environment variable as base64-encoded JSON.
- From 64 KB to 10 MB, it is written to a JSON file in the sandbox instead, and `AGPT_VARIABLES_FILE` holds that file's path. The variables are still set for you; read the file yourself only if you want the raw JSON.
- Over 10 MB, the block returns an error. Put the data at a URL (or in a file your code can fetch) and download it from your code.

JSON has no `NaN` or `Infinity`. In JavaScript they arrive as `null`, the same as `JSON.stringify` produces; Python keeps them as floats.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| setup_commands | Shell commands to set up the sandbox before running the code. You can use `curl` or `git` to install your desired Debian based package manager. `pip` and `npm` are pre-installed.  These commands are executed with `sh`, in the foreground. | List[str] | No |
| variables | Variables defined here can be used directly in your code. Each key (`variables_#_{name}`) is injected directly as a local variable with the same name (`{name}`) in your code. Values wired in from other blocks keep their type; default values set on this node come in as strings, so parse them in your code if you need a number or other type. Values arrive exactly as given (any quotes, backslashes or unicode); in JavaScript, NaN and Infinity arrive as null. Up to 10 MB of JSON in total. Up to 64 KB it is also in the `AGPT_VARIABLES` env var (base64 JSON); above that it is in the JSON file named by `AGPT_VARIABLES_FILE`. For more than 10 MB, put the data at a URL and download it in your code. | Dict[str, Any] | No |
| code | Code to execute in the sandbox | str | No |
| language | Programming language to execute | "python" \| "js" \| "bash" \| "r" \| "java" | No |
| timeout | Execution timeout in seconds | int | No |
| dispose_sandbox | Whether to dispose of the sandbox immediately after execution. If disabled, the sandbox will run until its timeout expires. | bool | No |
| template_id | You can use an E2B sandbox template by entering its ID here. Check out the E2B docs for more details: [E2B - Sandbox template](https://e2b.dev/docs/sandbox-template) | str | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| main_result | The main result from the code execution (the script's final expression). Its `json` sub-field is ONLY populated when the result is a dict/object/map — bare lists, strings, and numbers land in `text` as a string instead. To pass structured data downstream via `main_result_#_json_#_<key>` links, end the script with a key-value structure in the script's language (e.g. `{'items': my_list}` in Python, `({items: myList})` in JavaScript). | Main Result |
| results | List of results from the code execution | List[CodeExecutionResult] |
| response | Text output (if any) of the main execution result | str |
| stdout_logs | Standard output logs from execution | str |
| stderr_logs | Standard error logs from execution | str |
| files | Files created or modified during execution. Each file has path, name, content, and workspace_ref (if stored). | List[SandboxFileOutput] |

### Possible use case

**Data Processing**: Run Python scripts to transform, analyze, or visualize data that can't be handled by standard blocks.

**Custom Integrations**: Execute code to call APIs or services not covered by built-in blocks.

**Dynamic Computation**: Generate and execute code based on AI suggestions for flexible problem-solving.


---

## Execute Code Step

### What it is
Execute code in a previously instantiated sandbox.

### How it works

This block executes additional code in a sandbox that was previously created with the Instantiate Code Sandbox block. The sandbox maintains state between steps, so variables and installed packages persist.

Use this for multi-step code execution where each step builds on previous results. Set dispose_sandbox to true on the final step to clean up.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| sandbox_id | ID of the sandbox instance to execute the code in | str | Yes |
| step_code | Code to execute in the sandbox | str | No |
| language | Programming language to execute | "python" \| "js" \| "bash" \| "r" \| "java" | No |
| dispose_sandbox | Whether to dispose of the sandbox after executing this code. | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| main_result | The main result from the code execution (the script's final expression). Its `json` sub-field is ONLY populated when the result is a dict/object/map — bare lists, strings, and numbers land in `text` as a string instead. To pass structured data downstream via `main_result_#_json_#_<key>` links, end the script with a key-value structure in the script's language (e.g. `{'items': my_list}` in Python, `({items: myList})` in JavaScript). | Main Result |
| results | List of results from the code execution | List[CodeExecutionResult] |
| response | Text output (if any) of the main execution result | str |
| stdout_logs | Standard output logs from execution | str |
| stderr_logs | Standard error logs from execution | str |

### Possible use case

**Iterative Processing**: Load data in one step, transform it in another, and export in a third.

**Stateful Computation**: Build up results across multiple code executions with shared variables.

**Interactive Analysis**: Run exploratory data analysis steps sequentially in the same environment.


---

## Get Reddit Comment

### What it is
Get details about a specific Reddit comment by its ID.

### How it works

This block uses the Reddit API via PRAW to retrieve detailed information about a specific comment by its ID. Returns the comment content, author, score, timestamp, and other metadata.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| comment_id | The ID of the comment to fetch | str | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if comment couldn't be fetched | str |
| comment | The comment details | RedditComment |

### Possible use case

**Comment Analysis**: Analyze specific comments for sentiment or content moderation.

**Thread Tracking**: Monitor specific comments for engagement or replies.


---

## Get Reddit Comment Replies

### What it is
Get replies to a specific Reddit comment.

### How it works

This block uses the Reddit API via PRAW to fetch replies to a specific comment. Returns a list of direct replies with their content, authors, and metadata.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| comment_id | The ID of the comment to get replies from | str | Yes |
| post_id | The ID of the post containing the comment | str | Yes |
| limit | Maximum number of replies to fetch (max 50) | int | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if replies couldn't be fetched | str |
| reply | A reply to the comment | RedditComment |
| replies | All replies | List[RedditComment] |
| comment_id | The parent comment ID (pass-through for chaining) | str |
| post_id | The post ID (pass-through for chaining) | str |

### Possible use case

**Conversation Threading**: Build complete comment threads for analysis or display.

**Response Monitoring**: Track replies to your comments for engagement purposes.


---

## Get Reddit Inbox

### What it is
Get messages, mentions, and comment replies from your Reddit inbox.

### How it works

This block uses the Reddit API via PRAW to fetch items from your Reddit inbox. Filter by type to get all items, unread only, direct messages, username mentions, or replies to your comments.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| inbox_type | Type of inbox items to fetch | "all" \| "unread" \| "messages" \| "mentions" \| "comment_replies" | No |
| limit | Maximum number of items to fetch | int | No |
| mark_read | Whether to mark fetched items as read | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if fetch failed | str |
| item | An inbox item | RedditInboxItem |
| items | All fetched items | List[RedditInboxItem] |

### Possible use case

**Inbox Monitoring**: Check for new messages or mentions to respond to.

**Engagement Tracking**: Monitor comment replies to stay engaged with discussions.


---

## Get Reddit Post

### What it is
Get detailed information about a specific Reddit post by its ID.

### How it works

This block uses the Reddit API via PRAW to retrieve complete details about a specific post by its ID. Returns the post title, content, author, score, comment count, and other metadata.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| post_id | The ID of the post to fetch (e.g., 'abc123' or full ID 't3_abc123') | str | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the post couldn't be fetched | str |
| post | Detailed post information | RedditPostDetails |

### Possible use case

**Post Analysis**: Analyze specific posts for content quality or engagement metrics.

**Content Verification**: Verify post details before interacting with it programmatically.


---

## Get Reddit Post Comments

### What it is
Get top-level comments on a Reddit post.

### How it works

This block uses the Reddit API via PRAW to fetch top-level comments on a post. Configure the sort order and limit to control which comments are returned.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| post_id | The ID of the post to get comments from | str | Yes |
| limit | Maximum number of top-level comments to fetch (max 100) | int | No |
| sort | Sort order for comments | "best" \| "top" \| "new" \| "controversial" \| "old" \| "qa" | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if comments couldn't be fetched | str |
| comment | A comment on the post | RedditComment |
| comments | All fetched comments | List[RedditComment] |
| post_id | The post ID (pass-through for chaining) | str |

### Possible use case

**Sentiment Analysis**: Analyze comments to gauge community sentiment on a topic.

**Content Moderation**: Review comments for compliance with community guidelines.


---

## Get Reddit Posts

### What it is
This block fetches Reddit posts from a defined subreddit name.

### How it works

The block connects to Reddit using provided credentials, accesses the specified subreddit, and retrieves posts based on the given parameters. It can limit the number of posts, stop at a specific post, or fetch posts within a certain time frame.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| subreddit | Subreddit name, excluding the /r/ prefix | str | No |
| last_minutes | Post time to stop minutes ago while fetching posts | int | No |
| last_post | Post ID to stop when reached while fetching posts | str | No |
| post_limit | Number of posts to fetch | int | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| post | Reddit post | RedditPost |
| posts | List of all Reddit posts | List[RedditPost] |

### Possible use case

A content curator could use this block to gather recent posts from a specific subreddit for analysis, summarization, or inclusion in a newsletter.


---

## Get Reddit User Info

### What it is
Get information about a Reddit user including karma, account age, and verification status.

### How it works

This block uses the Reddit API via PRAW to retrieve public profile information about a Reddit user, including karma scores, account age, and verification status.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| username | The Reddit username to look up (without /u/ prefix) | str | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if user lookup failed | str |
| user | User information | RedditUserInfo |
| username | The username (pass-through for chaining) | str |

### Possible use case

**User Verification**: Check user account age and karma before engaging.

**User Research**: Gather user profile data for analysis or outreach decisions.


---

## Get Subreddit Flairs

### What it is
Get available link flair options for a subreddit.

### How it works

This block uses the Reddit API via PRAW to retrieve available link flair options for a subreddit. Use the flair IDs with the Create Reddit Post block to apply flair to your posts.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| subreddit | Subreddit name (without /r/ prefix) | str | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if fetch failed | str |
| flair | A flair option | SubredditFlair |
| flairs | All available flairs | List[SubredditFlair] |
| subreddit | The subreddit name (pass-through for chaining) | str |

### Possible use case

**Post Preparation**: Get available flairs before creating posts to ensure proper categorization.

**Flair Selection**: Present flair options to users or select appropriate flair programmatically.


---

## Get Subreddit Info

### What it is
Get information about a subreddit including subscriber count, description, and rules.

### How it works

This block uses the Reddit API via PRAW to retrieve metadata about a subreddit including subscriber count, description, creation date, and posting rules.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| subreddit | Subreddit name (without /r/ prefix) | str | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the subreddit couldn't be fetched | str |
| info | Subreddit information | SubredditInfo |
| subreddit | The subreddit name (pass-through for chaining) | str |

### Possible use case

**Subreddit Research**: Analyze subreddits before deciding to post or engage.

**Community Analysis**: Compare subreddit sizes and activity for market research.


---

## Get Subreddit Rules

### What it is
Get the rules for a subreddit to ensure compliance before posting.

### How it works

This block uses the Reddit API via PRAW to retrieve the posting rules for a subreddit. Review these rules before posting to ensure compliance.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| subreddit | Subreddit name (without /r/ prefix) | str | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if fetch failed | str |
| rule | A subreddit rule | SubredditRule |
| rules | All subreddit rules | List[SubredditRule] |
| subreddit | The subreddit name (pass-through for chaining) | str |

### Possible use case

**Compliance Check**: Review rules before automated posting to avoid violations.

**Content Guidelines**: Display rules to users before they submit content to a subreddit.


---

## Get User Posts

### What it is
Fetch posts by a specific Reddit user.

### How it works

This block uses the Reddit API via PRAW to fetch posts submitted by a specific user. Configure sort order and limit to control which posts are returned.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| username | Reddit username to fetch posts from (without /u/ prefix) | str | Yes |
| post_limit | Maximum number of posts to fetch | int | No |
| sort | Sort order for user posts | "new" \| "hot" \| "top" \| "controversial" | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if posts couldn't be fetched | str |
| post | A post by the user | RedditPost |
| posts | All posts by the user | List[RedditPost] |

### Possible use case

**User Analysis**: Analyze a user's posting history for content patterns or topics.

**Influencer Research**: Research prolific posters in specific communities.


---

## Instantiate Code Sandbox

### What it is
Instantiate a sandbox environment with internet access in which you can execute code with the Execute Code Step block.

### How it works

This block creates a persistent E2B sandbox environment that can be used for multiple code execution steps. Run setup_commands and setup_code to prepare the environment with dependencies and initial state.

The sandbox persists until its timeout expires or it's explicitly disposed. Use the returned sandbox_id with Execute Code Step blocks for subsequent code execution.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| setup_commands | Shell commands to set up the sandbox before running the code. You can use `curl` or `git` to install your desired Debian based package manager. `pip` and `npm` are pre-installed.  These commands are executed with `sh`, in the foreground. | List[str] | No |
| setup_code | Code to execute in the sandbox | str | No |
| language | Programming language to execute | "python" \| "js" \| "bash" \| "r" \| "java" | No |
| timeout | Execution timeout in seconds | int | No |
| template_id | You can use an E2B sandbox template by entering its ID here. Check out the E2B docs for more details: [E2B - Sandbox template](https://e2b.dev/docs/sandbox-template) | str | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| sandbox_id | ID of the sandbox instance | str |
| response | Text result (if any) of the setup code execution | str |
| stdout_logs | Standard output logs from execution | str |
| stderr_logs | Standard error logs from execution | str |

### Possible use case

**Complex Pipelines**: Set up an environment with data science libraries for multi-step analysis.

**Persistent State**: Create a sandbox with loaded models or data that multiple workflow branches can access.

**Custom Environments**: Configure specialized environments with specific package versions for reproducible execution.


---

## Lock Reddit Post

### What it is
Locks or unlocks a Reddit post or comment to prevent or allow replies. Requires 'modposts' scope. Reddit scopes are account-wide, so this grants the ability across every subreddit you moderate, not only the one set here.

### How it works

This block resolves the target `post_id` to the correct Reddit object from its `t3_` (post) or `t1_` (comment) prefix, so both can be locked or unlocked from the same input field; bare, unprefixed IDs are rejected as ambiguous. It then calls `lock()` when `lock=True` or `unlock()` when `lock=False`, using moderator credentials with the `modposts` scope.

The block returns the original `post_id` plus the resulting `locked` state, which makes it useful in review pipelines where the moderation decision and final state need to be recorded explicitly.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| post_id | Post or comment to lock or unlock. Full Reddit thing ID, prefixed with 't3_' for a post (e.g. 't3_abc123') or 't1_' for a comment (e.g. 't1_xyz789'). Bare IDs are rejected: posts and comments share an ID namespace, so an unprefixed ID cannot be resolved safely. | str | Yes |
| lock | True to lock (disable comments/replies), False to unlock | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| post_id | ID of the post (pass-through) | str |
| locked | Current lock state after the action | bool |

### Possible use case

**Thread Freeze**: Lock a post when discussion becomes abusive or starts attracting brigading.

**Post-Resolution Cleanup**: Unlock a thread again after moderators have resolved the incident and want to reopen discussion.

**Incident Playbooks**: Trigger lock or unlock actions automatically from moderation decision trees.


---

## Mod Queue

### What it is
Fetches the mod queue for a subreddit. Scalar outputs fan out once per queued post or comment; items emits the full batch once. Requires moderator access.

### How it works

This block calls `sub.mod.modqueue(...)` for the target subreddit, forwarding the optional `only` filter and the `limit` value (bounded to 1–100 so a single run can't fan out into an unbounded chain of paginated Reddit calls). Each returned PRAW item is normalized into a predictable dictionary with a fullname ID, detected item type, title fallback for comments, author, permalink, and moderator reason.

The block emits every queue entry individually for fan-out workflows and also emits the full `items` list for batch processing. Because the `post_id` output keeps the Reddit fullname (`t1_...` or `t3_...`), downstream moderation blocks can safely act on comments and submissions without extra type checks.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| subreddit | Subreddit name, excluding the /r/ prefix | str | Yes |
| limit | Maximum number of items to fetch from the mod queue (1-100) | int | No |
| only | Filter to only submissions or only comments. Leave blank for both. | "submissions" \| "comments" | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| post_id | Full Reddit thing ID of a queued item, such as 't3_abc123' or 't1_xyz789' | str |
| item_type | Whether the queued item is a comment or submission | "comment" \| "submission" |
| post_title | Title of the queued item | str |
| author | Username of the author | str |
| permalink | Full Reddit permalink | str |
| reason | Mod queue reason (if any) | str |
| items | All queued items as a list. Emitted exactly once; an empty list signals that the queue was checked and had no items. | List[Dict[str, Any]] |

### Possible use case

**Queue Triage**: Pull the latest subreddit queue and route each item into approve, remove, or lock actions.

**Moderator Dashboards**: Feed queued items into summaries, alerts, or external review systems for human moderators.

**Policy Automation**: Filter only comments or only submissions when building specialized moderation pipelines.


---

## Post Reddit Comment

### What it is
This block posts a Reddit comment on a specified Reddit post.

### How it works

The block connects to Reddit using the provided credentials, locates the specified post, and then adds the given comment to that post.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| post_id | The ID of the post to comment on | str | Yes |
| comment | The content of the comment to post | str | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| comment_id | Posted comment ID | str |
| post_id | The post ID (pass-through for chaining) | str |

### Possible use case

An automated moderation system could use this block to post pre-defined responses or warnings on Reddit posts that violate community guidelines.


---

## Publish To Medium

### What it is
Publishes a post to Medium.

### How it works

This block publishes articles to Medium using their API. Provide the content in HTML or Markdown format along with a title, tags, and publishing options. The author_id can be obtained from Medium's /me API endpoint.

Configure publish_status to publish immediately, save as draft, or make unlisted. The block returns the published post's ID and URL.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| author_id | The Medium AuthorID of the user. You can get this by calling the /me endpoint of the Medium API.  curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" https://api.medium.com/v1/me  The response will contain the authorId field. | str | No |
| title | The title of your Medium post | str | Yes |
| content | The main content of your Medium post | str | Yes |
| content_format | The format of the content: 'html' or 'markdown' | str | Yes |
| tags | List of tags for your Medium post (up to 5) | List[str] | Yes |
| canonical_url | The original home of this content, if it was originally published elsewhere | str | No |
| publish_status | The publish status | "public" \| "draft" \| "unlisted" | Yes |
| license | The license of the post: 'all-rights-reserved', 'cc-40-by', 'cc-40-by-sa', 'cc-40-by-nd', 'cc-40-by-nc', 'cc-40-by-nc-nd', 'cc-40-by-nc-sa', 'cc-40-zero', 'public-domain' | str | No |
| notify_followers | Whether to notify followers that the user has published | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the post creation failed | str |
| post_id | The ID of the created Medium post | str |
| post_url | The URL of the created Medium post | str |
| published_at | The timestamp when the post was published | int |

### Possible use case

**Content Syndication**: Automatically publish blog posts or newsletters to Medium to reach a wider audience.

**AI Content Publishing**: Generate articles with AI and publish them directly to Medium.

**Cross-Posting**: Republish existing content from other platforms to Medium with proper canonical URL attribution.


---

## Read RSS Feed

### What it is
Reads RSS feed entries from a given URL.

### How it works

This block fetches and parses RSS or Atom feeds from a URL. Filter entries by time_period to only get recent items. When run_continuously is enabled, the block polls the feed at the specified polling_rate interval.

Each entry is output individually, enabling processing of new content as it appears. The block also outputs all entries as a list for batch processing.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| rss_url | The URL of the RSS feed to read | str | Yes |
| time_period | The time period to check in minutes relative to the run block runtime, e.g. 60 would check for new entries in the last hour. | int | No |
| polling_rate | The number of seconds to wait between polling attempts. | int | Yes |
| run_continuously | Whether to run the block continuously or just once. | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| entry | The RSS item | RSSEntry |
| entries | List of all RSS entries | List[RSSEntry] |

### Possible use case

**News Monitoring**: Track industry news feeds and process new articles for summarization or alerts.

**Content Aggregation**: Collect posts from multiple RSS feeds for a curated digest or newsletter.

**Blog Triggers**: Monitor a competitor's blog feed to trigger analysis or response workflows.


---

## Reddit Get My Posts

### What it is
Fetch posts created by the authenticated Reddit user (you).

### How it works

This block uses the Reddit API via PRAW to fetch posts you've submitted to Reddit. Useful for managing or analyzing your own posting history.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| post_limit | Maximum number of posts to fetch | int | No |
| sort | Sort order for posts | "new" \| "hot" \| "top" \| "controversial" | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if posts couldn't be fetched | str |
| post | A post by you | RedditPost |
| posts | All your posts | List[RedditPost] |

### Possible use case

**Content Management**: Review and manage your Reddit posting history.

**Performance Tracking**: Analyze the engagement of your previous posts.


---

## Remove Reddit Post

### What it is
Removes a Reddit post or comment as a moderator. Requires 'modposts' scope. Reddit scopes are account-wide, so this grants the ability across every subreddit you moderate, not only the one set here.

### How it works

This block requires a prefixed Reddit thing ID (`t3_...` for a post, `t1_...` for a comment) and resolves it to the matching submission or comment before moderation; bare IDs are rejected because posts and comments share an ID namespace. It then calls `thing.mod.remove(...)`, forwarding the `spam` flag and the optional `mod_note`, which is capped at Reddit's 250-character moderator-note limit by the input schema.

The block returns the original `post_id` and a success flag so workflows can record or branch on the removal decision. Passing through the prefixed IDs emitted by `Mod Queue` lets the same flow moderate queued comments and submissions without extra conversion.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| post_id | Post or comment to remove. Full Reddit thing ID, prefixed with 't3_' for a post (e.g. 't3_abc123') or 't1_' for a comment (e.g. 't1_xyz789'). Bare IDs are rejected: posts and comments share an ID namespace, so an unprefixed ID cannot be resolved safely. | str | Yes |
| spam | Mark as spam (True) or just remove (False). Spam trains the filter. | bool | No |
| mod_note | Optional internal moderator note visible only to mods | str | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| post_id | ID of the removed post (pass-through) | str |
| success | Whether the removal succeeded | bool |

### Possible use case

**Spam Cleanup**: Remove obvious spam and optionally train Reddit's spam filter by marking it as spam.

**Comment Moderation**: Use fullnames from `Mod Queue` to remove problematic comments without additional lookup steps.

**Human-in-the-Loop Review**: Attach an internal moderator note when an automated rule removes borderline content.


---

## Reply To Reddit Comment

### What it is
Reply to a specific Reddit comment. Useful for threaded conversations.

### How it works

This block uses the Reddit API via PRAW to post a reply to an existing comment. The reply appears as a nested response in the comment thread.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| comment_id | The ID of the comment to reply to | str | Yes |
| reply_text | The text content of the reply | str | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if reply failed | str |
| comment_id | ID of the newly created reply | str |
| parent_comment_id | The parent comment ID (pass-through for chaining) | str |

### Possible use case

**Automated Responses**: Reply to comments that mention your product or brand.

**Conversation Engagement**: Participate in discussions by responding to relevant comments.


---

## Search Reddit

### What it is
Search Reddit for posts matching a query. Can search all of Reddit or a specific subreddit.

### How it works

This block uses the Reddit API via PRAW to search for posts matching your query. Optionally limit the search to a specific subreddit and configure sort order and time filters.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| query | Search query string | str | Yes |
| subreddit | Limit search to a specific subreddit (without /r/ prefix) | str | No |
| sort | Sort order for search results | "relevance" \| "hot" \| "top" \| "new" \| "comments" | No |
| time_filter | Time filter for search results | "all" \| "day" \| "hour" \| "month" \| "week" \| "year" | No |
| limit | Maximum number of results to return | int | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if search failed | str |
| result | A search result | RedditSearchResult |
| results | All search results | List[RedditSearchResult] |

### Possible use case

**Brand Monitoring**: Search for mentions of your product or company across Reddit.

**Topic Research**: Find discussions about specific topics or keywords.


---

## Send Authenticated Web Request

### What it is
Make an authenticated HTTP request with host-scoped credentials (JSON / form / multipart).

### How it works

This block makes HTTP requests with automatic credential injection based on the request URL's host. Credentials are managed separately and applied when the URL matches a configured host pattern.

Supports JSON, form-encoded, and multipart requests with file uploads. The response is parsed and returned along with separate error outputs for client (4xx) and server (5xx) errors.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| url | The URL to send the request to | str | Yes |
| method | The HTTP method to use for the request | "GET" \| "POST" \| "PUT" \| "DELETE" \| "PATCH" \| "OPTIONS" \| "HEAD" | No |
| headers | The headers to include in the request | Dict[str, str] | No |
| json_format | If true, send the body as JSON (unless files are also present). | bool | No |
| body | Form/JSON body payload. If files are supplied, this must be a mapping of form‑fields. | Dict[str, Any] | No |
| files_name | The name of the file field in the form data. | str | No |
| files | Mapping of *form field name* → Image url / path / base64 url. | List[str (file)] | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Errors for all other exceptions | str |
| response | The response from the server | Response |
| client_error | Errors on 4xx status codes | Client Error |
| server_error | Errors on 5xx status codes | Server Error |

### Possible use case

**Private API Access**: Call APIs that require authentication without exposing credentials in the workflow.

**OAuth Integrations**: Access protected resources using pre-configured OAuth tokens.

**Multi-Tenant APIs**: Make requests to APIs where credentials vary by host or endpoint.


---

## Send Email

### What it is
This block sends an email using the provided SMTP credentials.

### How it works

This block sends emails via SMTP using your configured email server credentials. Provide the recipient address, subject, and body content. The SMTP configuration includes server host, port, username, and password.

The block handles connection, authentication, and message delivery, returning a status indicating success or failure.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| to_email | Recipient email address | str | Yes |
| subject | Subject of the email | str | Yes |
| body | Body of the email | str | Yes |
| config | SMTP Config | SMTP Config | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the email sending failed | str |
| status | Status of the email sending operation | str |

### Possible use case

**Notification Emails**: Send automated notifications when workflow events occur.

**Report Delivery**: Email generated reports or summaries to stakeholders.

**Alert System**: Send email alerts when monitoring workflows detect issues or thresholds.


---

## Send Mod Mail

### What it is
Sends a modmail message from a subreddit to a user. Requires 'modmail' scope. Reddit scopes are account-wide, so this grants the ability across every subreddit you moderate, not only the one set here.

### How it works

This block opens the target subreddit and creates a modmail conversation via `sub.modmail.create(...)` using the provided recipient, subject, and body. Because it uses the subreddit modmail endpoint, the credential must include the `modmail` scope and moderator access to that community.

On success, the block returns the new conversation ID and `success=True`, which gives later steps a stable reference for logging or follow-up actions. Any Reddit API failure is surfaced through the standard `error` output.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| subreddit | Subreddit to send modmail from, excluding the /r/ prefix | str | Yes |
| to_username | Username to send the modmail to (without u/ prefix) | str | Yes |
| subject | Subject line of the modmail message | str | Yes |
| body | Body of the modmail message | str | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| conversation_id | ID of the created modmail conversation | str |
| success | Whether the modmail was sent | bool |

### Possible use case

**Appeal Responses**: Send official moderator replies when a user asks why content was removed or locked.

**Proactive Outreach**: Notify a user about rule issues before escalating to stronger moderation actions.

**Case Management**: Create modmail threads that can be referenced by later audit or follow-up steps.


---

## Send Reddit Message

### What it is
Send a private message (DM) to a Reddit user.

### How it works

This block uses the Reddit API via PRAW to send a private message to another Reddit user. The message appears in their inbox.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| username | The Reddit username to send a message to (without /u/ prefix) | str | Yes |
| subject | The subject line of the message | str | Yes |
| message | The body content of the message | str | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if sending failed | str |
| success | Whether the message was sent | bool |
| username | The username (pass-through for chaining) | str |

### Possible use case

**Outreach**: Send direct messages to users for collaboration or feedback requests.

**Support**: Provide private support or follow-up to users who engaged with your content.


---

## Send Web Request

### What it is
Make an HTTP request (JSON / form / multipart).

### How it works

This block makes HTTP requests to any URL. Configure the method (GET, POST, PUT, DELETE, PATCH), headers, and request body. Supports JSON, form-encoded, and multipart content types with file uploads.

The response body is parsed and returned. Separate error outputs distinguish between client errors (4xx), server errors (5xx), and other failures.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| url | The URL to send the request to | str | Yes |
| method | The HTTP method to use for the request | "GET" \| "POST" \| "PUT" \| "DELETE" \| "PATCH" \| "OPTIONS" \| "HEAD" | No |
| headers | The headers to include in the request | Dict[str, str] | No |
| json_format | If true, send the body as JSON (unless files are also present). | bool | No |
| body | Form/JSON body payload. If files are supplied, this must be a mapping of form‑fields. | Dict[str, Any] | No |
| files_name | The name of the file field in the form data. | str | No |
| files | Mapping of *form field name* → Image url / path / base64 url. | List[str (file)] | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Errors for all other exceptions | str |
| response | The response from the server | Response |
| client_error | Errors on 4xx status codes | Client Error |
| server_error | Errors on 5xx status codes | Server Error |

### Possible use case

**API Integration**: Call REST APIs to fetch data, trigger actions, or send updates.

**Webhook Delivery**: Send webhook notifications to external services when events occur.

**Custom Services**: Integrate with services that don't have dedicated blocks using their HTTP APIs.


---

## Transcribe Youtube Video

### What it is
Transcribes a YouTube video using a proxy.

### How it works

This block extracts transcripts from YouTube videos using a proxy service. It parses the YouTube URL to get the video ID and retrieves the available transcript, typically the auto-generated or manually uploaded captions.

The transcript text is returned as a single string, suitable for summarization, analysis, or other text processing.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| youtube_url | The URL of the YouTube video to transcribe | str | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Any error message if the transcription fails | str |
| video_id | The extracted YouTube video ID | str |
| transcript | The transcribed text of the video | str |

### Possible use case

**Video Summarization**: Extract video transcripts for AI summarization or key point extraction.

**Content Repurposing**: Convert YouTube content into written articles, social posts, or documentation.

**Research Automation**: Transcribe educational or informational videos for analysis and note-taking.


---

## Unban Subreddit User

### What it is
Unbans a user from a subreddit. Requires 'modcontributors' scope. Reddit scopes are account-wide, so this grants the ability across every subreddit you moderate, not only the one set here.

### How it works

This block opens the target subreddit with moderator credentials and calls `sub.banned.remove(username)` to remove the user from the community ban list. It requires the `modcontributors` scope, and on success it returns the `username`, `subreddit`, and `success=True` so the unban can be audited or chained into follow-up actions such as notifications.

Reddit is responsible for validating the username, subreddit, and moderator permissions. If the username is malformed, the subreddit is missing, or the credential does not have sufficient moderator access, the Reddit API error is surfaced through the block's standard `error` output. In practice, repeated unban attempts are typically safe to treat as idempotent workflow steps: if another moderator already removed the ban, the operation should be logged as a no-op for auditability, while transient API failures or rate limits should be retried with normal backoff before escalating to a human moderator.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| subreddit | Subreddit to unban the user from, excluding the /r/ prefix | str | Yes |
| username | Reddit username to unban (without the u/ prefix) | str | Yes |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| username | Unbanned username (pass-through) | str |
| subreddit | Subreddit (pass-through) | str |
| success | Whether the unban succeeded | bool |

### Possible use case

**Appeal Resolution**: Restore access after a moderator approves a user's ban appeal.

**Temporary Ban Expiry**: Pair scheduled workflows with unban actions when a manual review confirms the restriction should end.

**Moderator Remediation**: Correct mistaken bans and immediately hand the result to a notification step.


---

---

### Integrations Block Integrations Multimedia (`docs/integrations/block-integrations/multimedia.md`)

# Multimedia

Blocks for processing and manipulating video and audio files.


## Add Audio To Video

### What it is
Block to attach an audio file to a video file using moviepy.

### How it works

This block combines a video file with an audio file using the moviepy library. The audio track is attached to the video, optionally with volume adjustment via the volume parameter (1.0 = original volume).

Input files can be URLs, data URIs, or local paths. The output format is automatically determined: `workspace://` URLs in CoPilot, data URIs in graph executions.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| video_in | Video input (URL, data URI, or local path). | str (file) | Yes |
| audio_in | Audio input (URL, data URI, or local path). | str (file) | Yes |
| volume | Volume scale for the newly attached audio track (1.0 = original). | float | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| video_out | Final video (with attached audio), as a path or data URI. | str (file) |

### Possible use case

**Add Voiceover**: Combine generated voiceover audio with video content for narrated videos.

**Background Music**: Add music tracks to silent videos or replace existing audio.

**Audio Replacement**: Swap the audio track of a video for localization or accessibility.


---

## Loop Video

### What it is
Block to loop a video to a given duration or number of repeats.

### How it works

This block extends a video by repeating it to reach a target duration or number of loops. Set duration to specify the total length in seconds, or use n_loops to repeat the video a specific number of times.

The looped video is seamlessly concatenated. The output format is automatically determined: `workspace://` URLs in CoPilot, data URIs in graph executions.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| video_in | The input video (can be a URL, data URI, or local path). | str (file) | Yes |
| duration | Target duration (in seconds) to loop the video to. If omitted, defaults to no looping. | float | No |
| n_loops | Number of times to repeat the video. If omitted, defaults to 1 (no repeat). | int | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| video_out | Looped video returned either as a relative path or a data URI. | str |

### Possible use case

**Background Videos**: Loop short clips to match the duration of longer audio or content.

**GIF-Like Content**: Create seamlessly looping video content for social media.

**Filler Content**: Extend short video clips to meet minimum duration requirements.


---

## Media Duration

### What it is
Block to get the duration of a media file.

### How it works

This block analyzes a media file and returns its duration in seconds. Set is_video to true for video files or false for audio files to ensure proper parsing.

The input can be a URL, data URI, or local file path. The duration is returned as a float for precise timing calculations.


### Inputs

| Input | Description | Type | Required |
|-------|-------------|------|----------|
| media_in | Media input (URL, data URI, or local path). | str (file) | Yes |
| is_video | Whether the media is a video (True) or audio (False). | bool | No |

### Outputs

| Output | Description | Type |
|--------|-------------|------|
| error | Error message if the operation failed | str |
| duration | Duration of the media file (in seconds). | float |

### Possible use case

**Video Processing Prep**: Get video duration before deciding how to loop, trim, or synchronize it.

**Audio Matching**: Determine audio length to generate matching-length video content.

**Content Validation**: Verify that uploaded media meets duration requirements.


---

---

### Integrations Block Integrations Reddit (`docs/integrations/block-integrations/reddit.md`)

# Reddit Interaction Blocks

## Get Reddit Posts

### What it is
A block that retrieves posts from a specified subreddit on Reddit.

### What it does
This block fetches a set number of recent posts from a given subreddit, allowing users to collect content from Reddit for various purposes.

### How it works
The block connects to Reddit using provided credentials, accesses the specified subreddit, and retrieves posts based on the given parameters. It can limit the number of posts, stop at a specific post, or fetch posts within a certain time frame.

### Inputs
| Input | Description |
|-------|-------------|
| Subreddit | The name of the subreddit to fetch posts from |
| Reddit Credentials | Login information for accessing Reddit |
| Last Minutes | An optional time limit to stop fetching posts (in minutes) |
| Last Post | An optional post ID to stop fetching when reached |
| Post Limit | The maximum number of posts to fetch (default is 10) |

### Outputs
| Output | Description |
|--------|-------------|
| Post | A Reddit post containing the post ID, subreddit name, title, and body text |

### Possible use case
A content curator could use this block to gather recent posts from a specific subreddit for analysis, summarization, or inclusion in a newsletter.

---

## Post Reddit Comment

### What it is
A block that posts a comment on a specified Reddit post.

### What it does
This block allows users to submit a comment to a particular Reddit post using provided credentials and comment data.

### How it works
The block connects to Reddit using the provided credentials, locates the specified post, and then adds the given comment to that post.

### Inputs
| Input | Description |
|-------|-------------|
| Reddit Credentials | Login information for accessing Reddit |
| Comment Data | Contains the post ID to comment on and the comment text |

### Outputs
| Output | Description |
|--------|-------------|
| Comment ID | The unique identifier of the newly posted comment |

### Possible use case
An automated moderation system could use this block to post pre-defined responses or warnings on Reddit posts that violate community guidelines.

---

### Integrations Block Integrations Replicate Flux Advanced (`docs/integrations/block-integrations/replicate_flux_advanced.md`)

## Replicate Flux Advanced Model

### What it is
The Replicate Flux Advanced Model block is an AI-powered image generation tool that creates images based on text prompts and various customizable settings.

### What it does
This block generates high-quality images using advanced AI models from Replicate, specifically the Flux series of models. It allows users to input a text description and adjust various parameters to fine-tune the image generation process.

### How it works
The block takes a text prompt and several customization options as input. It then sends this information to the selected Flux model on the Replicate platform. The AI model processes the input and generates an image based on the provided specifications. Finally, the block returns a URL to the generated image.

### Inputs
| Input | Description |
|-------|-------------|
| API Key | Your Replicate API key for authentication |
| Prompt | A text description of the image you want to generate (e.g., "A futuristic cityscape at sunset") |
| Image Generation Model | Choose from Flux Schnell, Flux Pro, or Flux Pro 1.1 |
| Seed | An optional number for reproducible image generation |
| Steps | The number of diffusion steps in the image generation process |
| Guidance | Controls how closely the image follows the text prompt |
| Interval | Affects the variety of possible outputs |
| Aspect Ratio | The width-to-height ratio of the generated image |
| Output Format | Choose between WEBP, JPG, or PNG file formats |
| Output Quality | Image quality setting (0-100) for JPG and WEBP formats |
| Safety Tolerance | Content safety setting, from 1 (strictest) to 5 (most permissive) |

### Outputs
| Output | Description |
|--------|-------------|
| Result | A URL link to the generated image |
| Error | An error message if the image generation process fails |

### Possible use case
A graphic designer could use this block to quickly generate concept art for a sci-fi game. They might input a prompt like "A futuristic spaceport on a distant planet with multiple moons in the sky" and adjust the settings to get the desired style and quality. The generated image could then serve as inspiration or a starting point for further design work.
- API Key: Your Replicate API key for authentication
- Prompt: A text description of the image you want to generate (e.g., "A futuristic cityscape at sunset")
- Image Generation Model: Choose from Flux Schnell, Flux Pro, or Flux Pro 1.1
- Seed: An optional number for reproducible image generation
- Steps: The number of diffusion steps in the image generation process
- Guidance: Controls how closely the image follows the text prompt
- Interval: Affects the variety of possible outputs
- Aspect Ratio: The width-to-height ratio of the generated image
- Output Format: Choose between WEBP, JPG, or PNG file formats
- Output Quality: Image quality setting (0-100) for JPG and WEBP formats
- Safety Tolerance: Content safety setting, from 1 (strictest) to 5 (most permissive)

### Outputs
- Result: A URL link to the generated image
- Error: An error message if the image generation process fails

### Possible use case
A graphic designer could use this block to quickly generate concept art for a sci-fi game. They might input a prompt like "A futuristic spaceport on a distant planet with multiple moons in the sky" and adjust the settings to get the desired style and quality. The generated image could then serve as inspiration or a starting point for further design work.

---

### Integrations Block Integrations Rss (`docs/integrations/block-integrations/rss.md`)

# Read RSS Feed

## What it is
A block that retrieves and processes entries from an RSS feed.

## What it does
This block reads entries from a specified RSS feed URL, filters them based on a given time period, and outputs the entries one by one.

## How it works
The block connects to the provided RSS feed URL, fetches the feed content, and processes each entry. It checks if the entry's publication date falls within the specified time period and, if so, formats and outputs the entry information.

## Inputs
| Input | Description |
|-------|-------------|
| RSS URL | The web address of the RSS feed you want to read from |
| Time Period | The number of minutes to look back for new entries, relative to when the block starts running |
| Polling Rate | How often (in seconds) the block should check for new entries |
| Run Continuously | Whether the block should keep checking for new entries indefinitely or just run once |

## Outputs
| Output | Description |
|--------|-------------|
| Entry | An RSS feed item containing the following information: |
| | - Title: The headline or name of the item |
| | - Link: The web address where the full item can be found |
| | - Description: A brief summary or excerpt of the item |
| | - Publication Date: When the item was published |
| | - Author: Who wrote or created the item |
| | - Categories: Topics or tags associated with the item |

## Possible use case
A news aggregator application could use this block to continuously monitor multiple RSS feeds from different news sources. The application could then display the latest news items to users, categorized by topic and sorted by publication date.

---

--- METRICS ---
- Files Extracted: 97
- Estimated Token Budget: ~250362 tokens
- Canonical Reference: https://codewiki.google/github.com/Significant-Gravitas/AutoGPT
