A3 splits automation into the parts a model is good at — observation, decision, repair — and the parts code is good at — selectors, control flow, schemas. Each concept below corresponds to a real package in the monorepo.
A workflow declares the high-level objective — scrape hotel rates, file an expense — and binds it to a typed input schema, a typed output schema, and a directory of stage modules. You author this file; A3 uses it as the contract for learning and execution.
'src'. Used to resolve module paths.*.stage.ts.success: true.ctx.inputs in dev runs.Objective: scrape nightly room rates for a target hotel from public booking pages. For each date in the requested stay window, capture normalized room entries including room name, occupancy constraints, board type, cancellation policy, currency, tax and fee breakdown, and final payable price. Prefer API or network responses when available, and fall back to stable DOM extraction when required. Ensure outputs validate against the workflow schema and preserve source metadata needed for downstream diffing and replay verification.
export default defineWorkflow({ title: 'Scrape Rates', description: 'Scrape rates from a website', inputsSchema: ScrapeRatesInputSchema, outputsSchema: ScrapeRatesOutputSchema, rootDir: 'src', sitesDir: import.meta.dirname + '/sites', missionFile: import.meta.dirname + '/objectives/mission.md', agents: [TestAgent], projectLocations: [ { dir: objectivesDir, writable: true, patterns: ['*.md'] }, { dir: schemaDir, writable: false }, ], setup: async (ctx) => { ctx.inputs = sampleInputs; }, });
Each stage corresponds to a single recognisable state of the target service — a landing page,
a results table, a confirmation modal. Its match() answers “is this me?” with no side effects.
Its run() takes the actions that move the automation to the next state. During learning, A3 creates and updates these stage files.
ctx.outputs.true marks the stage as a terminal-success outcome.import { defineStage } from '@athree/runner'; export default defineStage({ title: 'Extract Rooms and Rates', description: 'Prefer availability JSON; scrape the grid if it is missing', success: true, match: async (ctx) => { const path = new URL(ctx.page.url()).pathname; if (!path.includes('/hotel/')) return false; return await ctx.page.locator('[data-rates-grid]').first().isVisible(); }, run: async (ctx) => { const intercepted = ctx.networkResources.find((r) => r.request.url.includes('getProductAvailability')); if (intercepted?.response?.responseBody) { const data = JSON.parse(intercepted.response.responseBody); ctx.outputs.hotelId = ctx.inputs.search.hotelId; ctx.outputs.results = [{ search: ctx.inputs.search, rooms: normalizeRooms(data), }]; return; } // No JSON: walk the rates table and normalize rows. ctx.outputs.results = await extractFromRateTable(ctx.page, ctx.inputs.search); }, });
Stages live under sites/<hostname>/. After your main.stage navigates,
the runner reads new URL(ctx.page.url()).hostname, slugifies it (dots → dashes),
and uses that as ctx.site. From then on only stages in that folder (plus the
default/ globals) are candidates — no cross-contamination, and you scale to
many services by adding folders.
sites/be-synxis-com/ → site be-synxis-com. No registry.sites/ ├── default/ │ └── main.stage.ts # global, navigates and sets ctx.site ├── be-synxis-com/ │ ├── main.stage.ts │ ├── extract-rates.stage.ts │ ├── inputs.yaml │ └── playbook.md ├── asp-hotel-story-ne-jp/ │ ├── main.stage.ts │ ├── inputs.yaml │ └── playbook.md └── www-rufinohotelpetitmendoza-com/ └── …
Some tasks resist deterministic code: extracting unstructured prose, classifying an unfamiliar layout, summarising error states. Wrap those in an agent — a system prompt, optional input/output schemas, optional tools. You author these agents; they return validated structured payloads or fail loudly.
icon takes a Font Awesome class.{ content }. Composed at agent invocation time.EvaluateTool, AskUserQuestionTool, FailTool.import { AskUserQuestionTool, defineAgent, EvaluateTool, FailTool, } from '@athree/runner'; export const TestAgent = defineAgent({ id: 'test_agent', title: 'Test Agent', description: 'A test agent', icon: 'fas fa-bug', systemPrompt: () => ({ content: `You assist the user with debugging…`, }), tools: [EvaluateTool, AskUserQuestionTool, FailTool], });
A tool is the smallest unit an agent can call. Strict input/output schemas, an execute
function, optional dependency-injected services. Define them once and reuse across agents.
export default defineTool({ description: 'Parses date in given format → ISO 8601', inputSchema: z.object({ text: z.string(), format: z.string(), }), outputSchema: z.string(), execute: async ({ text, format }, ctx) => { return parseToIso(text, format); }, });
Learning turns an objective and observed site behaviour into stage modules and a playbook.
In practice, the result is what matters: A3 writes and updates stage files plus playbook.md in your project.
*.stage.ts files under sites/<site>/.playbook.md describing known flow states and variants.NoStagesMatchError and MultipleStagesMatchError are first-class signals.
The orchestrator routes them to the right subagent rather than re-deriving intent from chat.
Each site directory holds a playbook.md next to its stages. Sections are stable so
the agent can patch them surgically. Playbooks describe entry points, flow states, variants and
known risks — the things stages must not re-derive on every run.
# Booking.com — Search rates ## Objective Retrieve nightly rates for a given hotel, date range and party composition. ## Entry points - Direct hotel URL with checkin/checkout query params - Homepage → search → first result ## Flow states 1. cookie-banner (overlay) 2. hotel-landing 3. rates-table (final, success) ## Variants - A/B: occupancy modal appears only when guests > 4 - Mobile layout served when UA is iOS/Android ## Known risks - 30s captcha challenge after ~12 navigations - Currency switch requires explicit ctx.input
The monorepo is layered: protocol packages own contracts, runtime packages own behaviour, and the frontend consumes typed RPC. Imports follow a topology — protos depend on nothing runtime, helpers depend on protos, runtime depends on both.