Create a new A3 automation project
npm install @athree/create-projectCreate and scaffold A3 automation workflows.
> Note: This template requires the next branch of @athree/runner.
> Last updated: 2026-01-27. This will change as this branch changes or is merged.
``bash`
npx @athree/create-project a3-project-starter
cd a3-project-starter
npm i && npm run compile
Naming convention: Projects should be named a3-project-{name} (e.g., a3-project-hotel-rates).
> Note: You must run npm run compile for the project to appear in your local athree runner.
Or clone manually:
`bash`
cp -r a3-project-template a3-project-starter
cd a3-project-starter
npm i && npm run compile
Use the Add Project button in the A3 runner UI to add your project directory. The runner will automatically detect workflows in compiled projects.
Configure your environment variables before running workflows:
`bash`
cp .env.example .envEdit .env and add your API keys
Required variables:
- At least one LLM API key: GOOGLEAI_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY
Optional variables:
- CHROME_HEADLESS — Run browser in headless mode (default: true)CHROME_KEEP_OPEN
- — Keep browser open between runs (default: true)CHROME_PERSISTENT
- — Use persistent browser profile (default: false). Required for extensions. See Chrome Extensions.EXTENSIONS_CONFIG_PATH
- — Path to your extensions.json file. See Chrome Extensions.
Browser extensions (e.g. captcha solvers, cookie consent handlers) can be automatically installed into the Chrome profile before your workflow runs. This requires four steps:
`bash`
npm install @athree/runner-extensions
This template ships with the idcac (I don't care about cookies) extension pre-installed in chrome-extensions/. To add more extensions, copy unpacked extension directories from a3-rate-shopper/chrome-extensions/ or other projects and add entries to extensions.json.
``
├── chrome-extensions/
│ ├── extensions.json # Extension manifest
│ ├── idcac/ # Pre-installed: cookie consent automation
│ └── {your-extension}/ # Add more as needed
Each extension must be an unpacked Chrome extension (a directory containing a manifest.json). You can get these by:a3-rate-shopper/chrome-extensions/
- Copying from an existing project (e.g. ).crx
- Downloading a file and extracting it
- Cloning the extension source from GitHub
The extensions.json file lists which extensions to load:
`json`
{
"extensions": [
{
"name": "idcac",
"path": "./chrome-extensions/idcac",
"description": "I don't care about cookies - Cookie consent automation",
"enabled": true,
"loadInPersistent": true,
"loadInTransient": true
}
]
}
To add a captcha solver, copy the directory and add an entry:
`json`
{
"name": "capsolver",
"path": "./chrome-extensions/capsolver",
"description": "CapSolver - Automated captcha solving extension",
"enabled": true,
"loadInPersistent": true,
"loadInTransient": false
}
Fields:
- name — Human-readable identifierpath
- — Relative path from the project root to the unpacked extension directorydescription
- — What the extension doesenabled
- — Set to false to skip without removing the entryloadInPersistent
- — Load when the browser runs in persistent mode (CHROME_PERSISTENT=true)loadInTransient
- — Load when the browser runs in transient mode (default)
Add these to your .env file:
`bash`
CHROME_PERSISTENT=true
EXTENSIONS_CONFIG_PATH=./chrome-extensions/extensions.json
> Important: Most captcha solver extensions set loadInPersistent: true and loadInTransient: false, so CHROME_PERSISTENT=true is required for them to load.
In your workflow's run() method, register the service and install extensions before calling getPage():
`typescript
import { BrowserService, workflow } from '@athree/runner';
import { BrowserExtensionsService } from '@athree/runner-extensions';
import { dep, Mesh } from 'mesh-ioc';
@workflow({ title: 'My Workflow' })
export class MyWorkflow {
@dep() private browserService!: BrowserService;
@dep() private browserExtensionsService!: BrowserExtensionsService;
@dep() private mesh!: Mesh;
async run() {
// 1. Register and install extensions (BEFORE getPage)
this.mesh.service(BrowserExtensionsService);
await this.browserExtensionsService.installExtensions(
true,
this.browserService.userDataDir,
);
// 2. Now get the page — extensions will be active
const page = await this.browserService.getPage();
await page.goto('https://example.com');
// ... rest of your workflow
}
}
`
Key points:
- this.mesh.service(BrowserExtensionsService) registers the service in the DI containerinstallExtensions(true, ...)
- — first argument is isPersistent (matches against loadInPersistent/loadInTransient in config)
- Extensions are installed into the Chrome user data directory, so they persist across runs
- The service uses Puppeteer internally to enable Chrome developer mode and load unpacked extensions
- Extensions only get installed once — subsequent runs detect they are already present and skip installation
``
├── chrome-extensions/ # Browser extensions (see Chrome Extensions)
│ ├── extensions.json # Extension manifest
│ └── idcac/ # Cookie consent handler (pre-installed)
├── datasets/ # Input YAML files (one per service)
├── services/ # Output directory (gitignored)
│ └── {service-id}/
│ ├── screenshot.png # Page screenshot
│ └── result.json # Workflow result
├── src/
│ ├── instructions/ # LLM instructions (markdown files)
│ ├── schema/ # Zod schemas (Dataset, Result)
│ └── workflows/ # Workflow classes
A minimal workflow demonstrating basic browser automation:
1. Navigates to a URL
2. Waits for page load
3. Takes a full-page screenshot
Useful as a starting point or for testing browser connectivity.
Demonstrates the GenericWebAgent pattern:GenericWebAgent
1. Navigates to a URL
2. Creates a via AgentsService
3. Runs the agent with a simple text objective
This is the simplest way to use LLM-powered web automation - no stages, no schemas, just a one-shot LLM call with browser tools.
Demonstrates the learning agent pattern with stages:
1. Loads instructions from a markdown file
2. Sets up automation context with input/output schemas
3. Uses AutomationRunner.learn() to execute with LLM guidance
4. The LLM creates reusable stages to navigate and extract data
Use this pattern for complex, multi-step automations that benefit from reusable stages.
1. Set up environment variables (see Environment Setup)
2. Run npm run compile to build the projectdatasets/
3. Update the dataset in the workflow (or load from )src/instructions/
4. Define instructions in for LLM-powered workflowssrc/schema/
5. Update schemas in to match your data
- ChatService — Send messages to the UIBrowserService
- — Access the browser page for screenshots, navigationAgentsService
- — Create LLM agents (createGenericWebAgent(), createPageExtractor())AutomationRunner
- — Run automations with run() (existing stages) or learn() (LLM-guided)AutomationContext
- — Manage automation state, inputs, outputs, and schemas
For simple one-shot LLM tasks, use GenericWebAgent:
`typescript
import { AgentsService, BrowserService, workflow } from '@athree/runner';
import { dep } from 'mesh-ioc';
@workflow({ title: 'My Workflow' })
export class MyWorkflow {
@dep() private browserService!: BrowserService;
@dep() private agentsService!: AgentsService;
async run() {
const page = await this.browserService.getPage();
await page.goto('https://example.com');
const agent = this.agentsService.createGenericWebAgent();
agent.addUserContext({ currentUrl: page.url() });
await agent.run('Extract the main heading and description from this page.');
}
}
`
For complex multi-step automations with reusable stages, use the learning agent:
`typescript
import { AutomationRunner, AutomationContext, workflow } from '@athree/runner';
import { dep } from 'mesh-ioc';
import { z } from 'zod';
@workflow({ title: 'My Workflow' })
export class MyWorkflow {
@dep() private automationRunner!: AutomationRunner;
@dep() private automation!: AutomationContext;
async run() {
const result = await this.automationRunner.learn({
automationId: 'my-automation',
instructions, // Loaded from .md file
inputs: { url }, // Input data
inputsSchema, // Zod schema for inputs
outputsSchema, // Zod schema for expected outputs
learningModel: 'gemini-2.5-flash', // Optional: specify LLM model
});
const outputs = this.automation.outputs;
}
}
`
Instructions tell the LLM how to handle different page states. Place them in src/instructions/:
`markdownGoal
Extract product details from the page.
- Product title
- ctx.outputs.price - Product price
- ctx.outputs.status - "success" or "failure"Instructions
Product page
1. Extract the title from h1
2. Extract the price
3. Set status to "success"
`Tips
- Define clear output fields in your instructions
- Use Zod schemas to validate inputs and outputs
- The LLM creates reusable stages that can be re-run without LLM
- Save results to the
services/` folder