# Axiom

> This skill should be used when the user asks to "build an axiom", "create an axiom", "make an automation that scrapes/clicks/fills/downloads/etc.", "set up a bot", "scrape this site", or otherwise wants browser automation built with Axiom — whether as a saved no-code axiom in their account or as a Node script using the @axiom_ai/api library. The skill also handles "I don't have an Axiom account" / "set me up" / "get me an API key" by walking the user through signup, login, and key minting. Emits one of two artifacts based on the user's intent and validates it before declaring done.

- Skill: `axiom-browser-automation/axiom` (Agent Skill, multi-file: 368 files)
- Install (CLI): `npx skillmds@latest add axiom-browser-automation/axiom`
- Raw SKILL.md: https://api.skillmd.com/api/skills/axiom-browser-automation/axiom/raw
- Safety review: pending (external: skill-scanner PASS, skillspector WARNING)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- License: ISC
- Author: axiom-browser-automation (https://skillmd.com/u/axiom-browser-automation)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/axiom-browser-automation/axiom

---


# Axiom builder skill

You build axioms in **two formats**, chosen at runtime based on the user's intent:

- **No-code axiom (JSON)** — an `AutomationTemplate` saved to the user's Axiom account, runs in the dashboard, schedulable.
- **Coded axiom (`@axiom_ai/api` JavaScript)** — 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/`](./workflows/) — see [`references/workflows-index.md`](./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: <PATH>"). The bash tool's CWD won't always match the skill directory, so a relative path will fail.

```bash
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-skills` then `/reload-plugins` to 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:

```bash
export AXIOM_API_KEY="axm_…"
```

Then proceed to Step 1.

**For (a) or (b) — they need a fresh key:**

1. **Ask only for what's needed.**
   - (a): just their email.
   - (b): their name and email.

2. **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.

3. **Run the bundled helper:**

   (Paths below use `<SKILL_BASE_DIR>` — substitute the absolute path announced at skill activation.)

   New account (b):
   ```bash
   node "<SKILL_BASE_DIR>/scripts/signup-and-mint-key.js" \
     --name "Their Name" --email their@email.com
   ```

   Existing account (a):
   ```bash
   node "<SKILL_BASE_DIR>/scripts/signup-and-mint-key.js" \
     --email their@email.com --existing
   ```

   The helper outputs three lines on success:
   ```
   API_KEY=axm_xxxxxxxxxxxxxxxxxxxx
   USER_ID=12345
   USER_EMAIL=their@email.com
   ```

   Capture the `API_KEY` line.

4. **Persist the key.** Read `~/.claude/settings.json`, add the key to the `env` block:

   ```json
   "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.

5. **Export for the current session.**
   ```bash
   export AXIOM_API_KEY="axm_…"
   ```

6. **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`](./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`](./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:**

1. `references/automation-template-schema.json` — authoritative schema your output must validate against.
2. `references/automation-template-schema.md` — prose explanation of the shape, with the minimal-required-fields cheatsheet.
3. `references/action-vocabulary.json` — the `machine_name` values you may use in widgets (`baseActionList` + `widgetActionList`).
4. `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 via `BuildNoCodeWorkflow` (see Step 3).

**For the coded path:**

1. `references/axiom-api-surface.md` — the only methods you may call. The full method allowlist with signatures.
2. `references/axiom-api-method-allowlist.json` — machine-readable allowlist the validator uses.
3. `references/axiom-api-method-blocklist.json` — methods the validator rejects (private + internal).
4. `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`](./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.md` entry and any
  step-reference entries about pagination loops.
- User mentions *Google Sheets* → read the matching entries from `no-code-tool/integrations/` plus the
  `WidgetWriteGoogleSheet` step 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.md` if 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:

```bash
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:**

1. **Compose an intent JSON** describing the axiom at the high level. `machineName` must be a value from `references/action-vocabulary.json`'s `widgetActionList`. `values` keys must be the exact param names declared by that widget (case-sensitive — `"Enter URL"`, not `"URL"`).

   ```json
   {
     "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']"}}
     ]
   }
   ```

2. **Invoke the workflow** with the intent + the user's chosen output path:

   ```bash
   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 a `response.message` containing 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.

3. **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.js` or `scripts/validate-no-code.js` directly. 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:

1. **Wire the iteration data** with `tokenRefs`, not `values`. The BotCreate step has a `Loop through data` param of type `bot_token`; in the intent, point it at the upstream step's `token`:
   ```js
   { machineName: 'WidgetBotCreate',
     stepNumber: '3',
     tokenRefs: { 'Loop through data': 'google-sheet-data' } }
   ```
   The helper emits `value: "[google-sheet-data]"` (the canonical `bot_token` runtime shape — a single bracketed string, not an array; see the per-type shape table below).
2. **Body steps use sub-numbered `stepNumber` labels** (`"3.1"`, `"3.2"`, …) under the BotCreate's parent number. The helper takes a `stepNumber` override on each body step:
   ```js
   { machineName: 'WidgetDriverEnterText', stepNumber: '3.1', values: { ... } }
   ```
3. **The closing `WidgetBotComplete` step 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`](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 `TemplateLoopThroughData` directly — 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]` |
| Email | 2 | `[google-sheet-data?*&2]` |
| Gender | 3 | `[google-sheet-data?*&3]` |
| Mobile | 4 | `[google-sheet-data?*&4]` |
| Current Address | 9 | `[google-sheet-data?*&9]` |

```js
// 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 cell` to `A2`** so 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 for `bot_token`-typed params like the loop's `Loop 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:

```js
{ 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 like `goto`, `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:

```bash
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 '<name>' 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:

1. **User says yes** → run the `saveCommand` via 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 '<name>' to your Axiom account ✓"* and stop.
   - On `ok: false`: tell the user *"Save failed: <error>"* and fall through to the import flow below.
2. **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`.
3. **Extension not installed** → invoke `HandoffToExtensionWorkflow` for 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`](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.

