Axiom builder skill
You build axioms in two formats, chosen at runtime based on the user's intent:
- No-code axiom (JSON) — an
AutomationTemplatesaved to the user's Axiom account, runs in the dashboard, schedulable. - Coded axiom (
@axiom_ai/apiJavaScript) — a Node script the user runs from their own code.
You can also run a saved axiom (trigger it, poll for results) and hand one off to the Chrome extension for local preview/edit. These five capabilities are encoded as workflows in workflows/ — see references/workflows-index.md for the full registry.
Before you produce anything, decide which workflow fits. If the user's intent is ambiguous, ask one clarifying question.
Step −1 — Check for a newer skill version (once per conversation)
The first time the axiom skill activates in a conversation, run the update check via the skill's bundled script. Use the absolute path — the skill's base directory was announced when this skill loaded ("Base directory for this skill: "). The bash tool's CWD won't always match the skill directory, so a relative path will fail.
node "<SKILL_BASE_DIR>/scripts/check-for-updates.js"
Replace <SKILL_BASE_DIR> with the absolute path you were given at skill activation.
If it prints a line starting with UPDATE_AVAILABLE, surface a one-line note to the user before continuing with their request:
Heads up: skill v<latest> is available (you're on v<current>). Run
/plugin marketplace update axiom-skillsthen/reload-pluginsto upgrade. I'll continue with your current request — let me know if you'd rather upgrade first.
If the script prints nothing, proceed silently.
Don't re-run the check in the same conversation. Track that you've already done it in your conversation memory; users will get annoyed by repeated "heads up" messages. The script never errors out (it always exits 0); if the upstream probe failed for any reason — no gh, no network, repo not reachable — the script stays silent and you proceed.
Step 0 — Make sure the user has an API key
Skip this section if AXIOM_API_KEY is already set in the environment AND the user hasn't said anything like "I don't have an account" / "set me up". Otherwise walk through it before producing any artifacts.
Quick check: run [ -n "$AXIOM_API_KEY" ] && echo "AXIOM_API_KEY=set" || echo "AXIOM_API_KEY=unset" via Bash. Do not echo the raw value — it will end up in the conversation transcript. If it prints unset, you need to onboard.
You also need to do this if a save attempt later returns HTTP 401 — the key is stale or invalid.
Decision tree
Ask the user one question:
"Do you have an Axiom account already? (a) Yes, I just haven't minted an API key yet, (b) No, sign me up, (c) I already have a key — let me paste it."
For (c) — they have a key:
Take the key. Write it to ~/.claude/settings.json's env block under AXIOM_API_KEY. Tell the user to restart Claude Code for it to pick up. Don't store the key anywhere else — settings.json is the canonical location. For the current Bash session, also export it inline:
export AXIOM_API_KEY="axm_…"
Then proceed to Step 1.
For (a) or (b) — they need a fresh key:
Ask only for what's needed.
- (a): just their email.
- (b): their name and email.
Never take the password in chat. Tell the user:
"Set your password in your shell so it doesn't end up in our chat history:
export AXIOM_PASSWORD='your-password-here'. I'll read it from the env and won't see the value."Wait for them to confirm they've done it.
Run the bundled helper:
(Paths below use
<SKILL_BASE_DIR>— substitute the absolute path announced at skill activation.)New account (b):
node "<SKILL_BASE_DIR>/scripts/signup-and-mint-key.js" \ --name "Their Name" --email their@email.comExisting account (a):
node "<SKILL_BASE_DIR>/scripts/signup-and-mint-key.js" \ --email their@email.com --existingThe helper outputs three lines on success:
API_KEY=axm_xxxxxxxxxxxxxxxxxxxx USER_ID=12345 USER_EMAIL=their@email.comCapture the
API_KEYline.Persist the key. Read
~/.claude/settings.json, add the key to theenvblock:"env": { "AXIOM_API_KEY": "axm_…", "AXIOM_LAR_URL": "https://lar.axiom.ai" }Tell the user the key is persisted and will be available in future Claude Code sessions.
Export for the current session.
export AXIOM_API_KEY="axm_…"Warn about rotation. Minting a key invalidates any prior key on the account. If the user mentioned existing integrations, point that out — they'll need to update those integrations with the new key.
See references/account-setup.md for the full reference: edge cases, error modes, and the underlying flow.
Step 1 — Pick a path (and check the intent isn't "fix-my-existing-axiom")
Read references/decision-tree.md for the rules. Quick summary:
| Signal | Path |
|---|---|
| "every day at 9am", "schedule", "save to my account", "in my dashboard" | No-code |
| "Node script", "from my CI", "in my repo", "TypeScript", names a language | Coded |
| "Build me a bot", "scrape this page" with no other context | Ask |
| "my axiom is failing", "the click is wrong", "stuck on Cloudflare", "page won't load" | Troubleshooting — see below |
Don't emit both. One artifact per request.
Troubleshooting intents
If the user's prompt sounds like a complaint about an existing automation rather than a build
request, the skill is the wrong tool. Consult references/workflows/_catalog.json — the
troubleshooting-category entries describe the symptoms each in-app flow handles. If the user's
prompt matches one of those descriptions, tell them:
"This sounds like a troubleshooting issue on a running automation. The skill builds new axioms — it doesn't fix live runs. Open the failing automation in the Axiom dashboard and use the chat there (it has access to the run logs and can apply fixes in-place). The flow that handles this is
<workflow-key>."
Don't try to re-implement the troubleshooting flow. Those in-app flows depend on multi-turn session state (pending operations, hook bus, return stack) that has no analog in a single-turn Claude session.
Step 2 — Read the relevant reference material
For the no-code path:
references/automation-template-schema.json— authoritative schema your output must validate against.references/automation-template-schema.md— prose explanation of the shape, with the minimal-required-fields cheatsheet.references/action-vocabulary.json— themachine_namevalues you may use in widgets (baseActionList+widgetActionList).examples/no-code/*.json— three canonical reference AutomationTemplates regenerated through the build-axiom helper. Read for shape reference; don't hand-copy — produce your own JSON viaBuildNoCodeWorkflow(see Step 3).
For the coded path:
references/axiom-api-surface.md— the only methods you may call. The full method allowlist with signatures.references/axiom-api-method-allowlist.json— machine-readable allowlist the validator uses.references/axiom-api-method-blocklist.json— methods the validator rejects (private + internal).examples/coded/*.js— three golden patterns: simple scrape, login-then-extract, parallel sessions.
Step 2.5 — Consult the docs index for relevant deep-dives
The skill ships the full axiom.ai user docs (332 files across 41 categories — step references,
troubleshooting recipes, integration guides, API deep-dives) under references/docs/. Always
read references/docs/_index.json before composing the artifact.
The index has one entry per file with path, title, description and (where present) docCategory
and order. Scan it to find 3-6 docs whose title + description match the user's prompt — for
example:
- User says "scrape paginated results" → read the
no-code-tool/how-it-works/loop.mdentry and any step-reference entries about pagination loops. - User mentions Google Sheets → read the matching entries from
no-code-tool/integrations/plus theWidgetWriteGoogleSheetstep reference. - User reports an error message → look in
no-code-tool/troubleshooting/errors/for a match.
Don't read everything. The index exists so you pick precisely. Reading 3-6 well-chosen docs is always better than scanning the whole corpus. If nothing matches, skip this step rather than reading indiscriminately.
Always read:
references/decision-tree.mdif uncertain which path.
Step 3 — Pick the output path, then emit the artifact
Ask the user where they want the file saved before you write it. Propose ~/Downloads/axiom-<short-name>.json (or .js for the coded path) as the default — the Downloads folder is what the user's file picker opens in when they go to import via the Chrome extension's Cog → Import or download → Select file flow, so it's the path of least friction. Let them override with anything else if they prefer (/tmp/..., a project directory, etc.).
Resolve ~ to the user's home directory before writing — most file-writing tools don't expand the tilde themselves. Quick way:
echo "$HOME/Downloads/axiom-<short-name>.json"
If ~/Downloads doesn't exist on the user's machine (rare — present on macOS, Windows, and most Linux distros), fall back to ~/ and tell the user.
No-code path — invoke BuildNoCodeWorkflow with an intent. Do NOT hand-compose JSON.
The workflow is the single entry point for the no-code path. It takes a high-level intent (the user's automation name + a list of {machineName, values} step intents) and runs the full pipeline internally: build the canonical AutomationTemplate from widgetActionList, validate it, write it to disk. Hand-composing JSON is structurally unsafe — every step needs original_name + the widget's full param list with declared types + full metadata, and missing fields render as undefined: … in the Chrome extension. The workflow handles all of that.
Workflow:
Compose an intent JSON describing the axiom at the high level.
machineNamemust be a value fromreferences/action-vocabulary.json'swidgetActionList.valueskeys must be the exact param names declared by that widget (case-sensitive —"Enter URL", not"URL").{ "name": "BBC search for harry kane", "description": "Navigates to bbc.co.uk and searches for harry kane.", "contextUrl": "https://www.bbc.co.uk", "steps": [ {"machineName": "WidgetDriverGoto", "values": {"Enter URL": "https://www.bbc.co.uk"}}, {"machineName": "WidgetDriverEnterText", "values": {"Select text field": "input[type='search']", "Text": "harry kane"}}, {"machineName": "WidgetDriverClick", "values": {"Select": "button[type='submit']"}} ] }Invoke the workflow with the intent + the user's chosen output path:
node "<SKILL_BASE_DIR>/workflows/index.js" invoke build_no_code "$(cat <<'JSON' { "intent": { ...the intent JSON from step 1... }, "outputPath": "~/Downloads/axiom-bbc-search.json" } JSON )"The workflow's
invoke()returns aresponse.messagecontaining the absolute path the file was written to and the 4-step Chrome-extension import flow. If the intent has a typo (wrong widget name, wrong param key, etc.), invoke fails with a clear message naming the right alternative — fix and re-run.Hand the path + import flow back to the user. That's the end of the no-code path. No separate build / validate / save calls; the workflow does the lot.
⚠️ Don't run
scripts/build-axiom.jsorscripts/validate-no-code.jsdirectly. They exist as a power-user / CI escape hatch, but the workflow is the supported path. Don't hand-write JSON either — the strengthened validator catches the failure modes and refuses to declare a hand-composed artifact done.
Loops — repeat a body of steps once per row of data
When the user wants "for each row in this sheet, do X" / "visit each of these links" / similar, the no-code JSON has a specific shape that the helper now supports directly. There is no single "loop" widget; a loop is a WidgetBotCreate step (the start) + body steps + a WidgetBotComplete step (the end), with three conventions:
- Wire the iteration data with
tokenRefs, notvalues. The BotCreate step has aLoop through dataparam of typebot_token; in the intent, point it at the upstream step'stoken:
The helper emits{ machineName: 'WidgetBotCreate', stepNumber: '3', tokenRefs: { 'Loop through data': 'google-sheet-data' } }value: "[google-sheet-data]"(the canonicalbot_tokenruntime shape — a single bracketed string, not an array; see the per-type shape table below). - Body steps use sub-numbered
stepNumberlabels ("3.1","3.2", …) under the BotCreate's parent number. The helper takes astepNumberoverride on each body step:{ machineName: 'WidgetDriverEnterText', stepNumber: '3.1', values: { ... } } - The closing
WidgetBotCompletestep shares the BotCreate's stepNumber (both"3"). The importer pairs them by matching number.
The helper also handles two BotCreate-only top-level flags (isLooping: true, afterLoopUpdate: true) — set automatically from the vocab.
See examples/no-code/loop-through-sheet.json for a fully-wired ReadGoogleSheet → Goto → BotCreate(loop) → Wait → BotComplete flow that round-trips through the validator.
Don't try to emit
TemplateLoopThroughDatadirectly — that's the extension's macro name, not a single widget. The JSON is always the start/body/end triple above.
Token references between steps (the general pattern)
Anything in the intent that needs to consume another step's output (Continue widget's Data to check, BotCreate's Loop through data, a Sheet/CSV/Excel writer's DATA, etc.) goes through tokenRefs, NOT values. The helper rejects a tokenRef against a non-token-typed param, so typos surface as errors instead of silent literals.
The value shape is NOT universal — the runtime in axiom_lib/lib/execution/ExecutorJson.ts branches on the param's type and expects different shapes. The helper builds the right one for you, but if you're inspecting the output (or hand-patching an axiom), the per-type rules are:
Param type |
Value shape the helper emits | Notes |
|---|---|---|
token |
"[<name>]" (single string) |
Most common — used by Continue, AI steps, single-input data consumers. |
bot_token |
"[<name>]" |
BotCreate's Loop through data. One token only. |
merge_token_list |
"[<name>]" |
FilterMerge's Base data / Join data. Despite the _list suffix, takes one token. |
row_numbering_token |
"[<name>]" |
RowNumbers input. |
write_google_sheet_token |
"[<name>]" |
Google Sheet writer's DATA (single string, not array). |
write_csv_data_token |
"[<name>]" |
CSV writer's DATA. |
write_excel_sheet_token |
"[<name>]" |
Excel writer's DATA. |
token_list |
"[<name1>]\n[<name2>]" (newline-separated string) |
Multi-token slot. Pass an array of names to the helper; it joins with \n. |
merge_token |
"[<name1>]\n[<name2>]" |
Same — newline-separated string when multiple. |
Pre-v0.8.3 the helper always produced the array shape ["[<name>]"] for every type, which was silently broken at runtime (downstream steps got the unresolved literal string). If you're maintaining axioms produced by older versions, re-run them through BuildNoCodeWorkflow to repair the value shape; the helper's coerceTokenValue will unwrap legacy arrays automatically when you re-process the intent.
Referencing a single COLUMN of row data inside a text field or script (?*& format)
This is a DIFFERENT mechanism from the token-typed params above, and it is the one most likely to be gotten wrong. The tables above cover token-typed params (bot_token, token, …) that consume a whole upstream token. But when you need one specific column of a sheet-read / loop row plugged into a plain literal param — an Enter text step's Text (type long_text_required), a Go to page URL, a Write javascript Script, a Display a message, etc. — you embed a column reference directly in the values string. These params are NOT token-typed, so tokenRefs does not apply; the reference is literal text the runtime resolves.
The correct format is index-based, zero-based, with a ?*& separator:
[<token>?*&<0-based-column-index>]
Examples for a WidgetReadGoogleSheet step whose token is google-sheet-data, columns First Name | Last Name | Email | Gender | Mobile | … | Current Address:
| Column (header) | Index | Reference to embed |
|---|---|---|
| First Name | 0 | [google-sheet-data?*&0] |
| Last Name | 1 | [google-sheet-data?*&1] |
| 2 | [google-sheet-data?*&2] |
|
| Gender | 3 | [google-sheet-data?*&3] |
| Mobile | 4 | [google-sheet-data?*&4] |
| Current Address | 9 | [google-sheet-data?*&9] |
// Enter text step — plug column 0 (First Name) of the current loop row:
{ machineName: 'WidgetDriverEnterText',
values: { 'Select text field': '#firstName', 'Text': '[google-sheet-data?*&0]' } }
// Inside a Write javascript step — the runtime substitutes the cell value before running:
// var g = `[google-sheet-data?*&3]`.trim(); // Gender column
- Do NOT use
[google-sheet-data:Column Name](colon + header name). That is wrong — it renders as literal text in the field and the step does nothing useful. The separator is?*&and the selector is a numeric index, not the header string. - Inside a
Loop through data,[<read-step-token>?*&N]resolves to column N of the current row. Reference the read step's token (e.g.google-sheet-data), not the loop's own output token. - When you use index-based column refs, set the read step's
First celltoA2so the header row isn't looped as a bogus data row (there's no header-name lookup to preserve). - The whole-row reference (no
?*&) is[google-sheet-data]— used forbot_token-typed params like the loop'sLoop through data. - If a value must be bound in the extension instead, leave it blank; the user picks the column via Insert Data. But prefer emitting the correct
?*&reference so the axiom runs on import.
Multi-column scraping (SmartScraper / ScrapeLinks)
When a user wants more than one field per row (title + price + stock, etc.), WidgetDriverSmartScraper's Select param accepts an array of column specs, one per field. Pass it through values and the helper fills in the UI-only defaults:
{ machineName: 'WidgetDriverSmartScraper',
values: {
'Select': [
{ selector: 'article.product_pod h3 a', resultType: 'textContent' },
{ selector: 'article.product_pod .price_color', resultType: 'textContent' },
{ selector: 'article.product_pod .availability', resultType: 'textContent' },
],
'Max results': '20',
},
token: 'scrape-data' }
Each column needs selector (CSS) and optionally resultType ("textContent" by default; can also be "innerHTML", "link", "axiom-download"). The runtime extracts those two fields per column and ignores the rest. Scope each column's CSS to a row container (e.g. article.product_pod h3 a) so results align across columns. The legacy single-string-selector form ('Select': 'article.product_pod') still works for single-column scrapes — the helper recognises strings and passes them through.
Schedules + other top-level shape
triggers is empty [] unless the user wants a schedule. Schedule shape: {name, status: "active", type: "recursive", time_criteria, interval_type, starting_time}. Pass it through intent.triggers and the helper forwards it. The starting URL goes in intent.contextUrl. The rest of the envelope (id: 0, openWidgetIndex: -1, data.injector, data.templateItem, etc.) is handled by the helper — you don't need to think about it.
Coded rules
- Always
import { AxiomApi } from '@axiom_ai/api'. No other SDK import paths. - Read the token from
process.env.<NAME>— never inline a literal. - Wrap step calls in
try { … } finally { await axiom.browserClose() }. - Only call methods listed in
references/axiom-api-method-allowlist.json. - Never call
step()directly (it's the internal dispatcher — emit the named method likegoto,click,scrape). - Never call
_-prefixed methods.
Step 4 — Validate before declaring done (coded path only)
The no-code path validates inside BuildNoCodeWorkflow — nothing extra to do. The coded path needs an explicit validator pass:
node "<SKILL_BASE_DIR>/scripts/validate-coded.js" /tmp/your-script.js
Exit 0 = valid. Exit 1 = error codes printed (UNKNOWN_METHOD, MISSING_LIFECYCLE, HARDCODED_TOKEN, …). Fix and re-run. Don't argue with the validator — its rules come from the published @axiom_ai/api surface.
Step 5 — Hand the artifact back to the user
Confirm before saving, scheduling, or running
Saving to the user's account, attaching a schedule, and triggering a run (run_automation) all either write to their account or consume paid cloud runtime. Always state plainly what is about to happen and get an explicit yes before doing it — e.g. "This will save '' to your Axiom account" or "This will trigger a run and use your cloud runtime quota." Never save or trigger a run without a clear go-ahead, and confirm a second time for anything irreversible (placing an order, submitting a form, sending a message). When in doubt, ask.
No-code: offer to save it to their account first
BuildNoCodeWorkflow.invoke() returns a response.message that asks the user whether they want to save the axiom directly to their Axiom account. Relay that message verbatim and wait for their answer. The workflow's response.data carries the saveCommand (an absolute node scripts/save-automation.js --artifact <path> invocation) and response.nextSteps gives you the exact branching logic.
The flow:
- User says yes → run the
saveCommandvia Bash. It prints a single-line JSON to stdout:{ok: true, name, id}on success,{ok: false, error, status?}on failure.- On
ok: true: tell the user "Saved '' to your Axiom account ✓" and stop. - On
ok: false: tell the user "Save failed: " and fall through to the import flow below.
- On
- User says no, or the save failed → walk them through the manual-import flow: open the Chrome extension's builder, click the Cog icon, open "Import or download" → "Select file" → pick the JSON path → Save. Docs in
response.data.importDocsUrl. - Extension not installed → invoke
HandoffToExtensionWorkflowfor install guidance, then loop back to step 2.
The save script needs AXIOM_API_KEY in env (Step 0 already ensures that) and AXIOM_LAR_URL if pointing at a non-prod LAR (defaults to https://lar.axiom.ai).
Coded: hand the script back to the user
"I've written the script to <path>. To run it: npm install @axiom_ai/api, set AXIOM_API_KEY in your environment, then node <path>. The script uses your account's automation token — see references/axiom-api-surface.md for the methods used."
Failure modes you'll see
| Symptom | What it means | Fix |
|---|---|---|
Schema keyword: 'required' with missingProperty: 'X' |
Top-level or data.X field is missing |
Add it. See the minimal envelope in examples/no-code/visit-example.json. |
Schema keyword: 'enum' on a machine_name |
You used a widget name that doesn't exist | Pick the right one from action-vocabulary.json. |
Schema keyword: 'additionalProperties' |
You added a field the schema doesn't recognise | Remove it. The schema is strict (additionalProperties: false). |
Coded UNKNOWN_METHOD |
You called axiom.<foo> where foo isn't in the allowlist |
Check axiom-api-surface.md and pick a real method. |
Coded MISSING_LIFECYCLE |
Missing try { … } finally { await axiom.browserClose() } |
Wrap step calls. |
Coded HARDCODED_TOKEN |
You inlined the API key as a string literal | Replace with process.env.AXIOM_API_KEY. |
Coded INTERNAL_METHOD |
You called axiom.step(...) directly |
Emit the named method (goto, click, etc.) instead. |
User-reported runtime errors
If the user pastes or describes one of these after their axiom ran (vs failures you encountered while generating it), follow the recovery in the right column. Anything not on this list — point them at the dashboard's run reports.
| Symptom | What it means | What to tell the user |
|---|---|---|
| "Google access token has expired" / "Your Google access token is invalid or has expired" / Google Sheets steps suddenly returning permission errors after working before | Google has revoked the OAuth token axiom.ai uses on the user's behalf (security rotation, account change, or the user revoked it). Not an axiom bug — Google controls it. | Tell the user to click the Axiom extension icon in their browser, open Google Sheets and API key, and click Connect Google Sheets to re-grant access. Once reconnected, re-run the automation. See references/docs/no-code-tool/troubleshooting/errors/integrations/google-sheets.md for the canonical version. |
What this skill won't do
- It doesn't run the axiom. That's the user's job — either via the dashboard (no-code) or
node script.js(coded). - It doesn't troubleshoot live runs beyond the table in "User-reported runtime errors" above. For anything else, point the user at the dashboard's run reports.