SEO Automation

Codex Exec SEO Automation: Cron Jobs, Reports and CI

Automate SEO checks with codex exec: save reports with -o, gate posts with --output-schema, schedule drift checks with cron, and keep API keys safe in CI.

By Somanath Diksangi12 min read
Codex exec SEO automation: a scheduled script runs $seo drift, $seo audit and $blog seo-check and saves pass or fail reports

Codex can run your SEO checks while nobody is at the keyboard: codex exec starts a task from a script, a cron job or a CI step, and prints only the final message so you can save it, compare it or fail a build on it. That makes codex exec SEO automation a short shell script around commands you already know, such as $seo drift, $seo audit and $blog seo-check. This guide shows saved reports, sandbox settings, logs, pass/fail gates, schedules and CI.

Key takeaways

  • codex exec runs Codex without the interactive UI. Progress goes to stderr, only the final message goes to stdout, and -o saves that message as a report.
  • The default sandbox is read-only. Add --sandbox workspace-write only when the task should write files, such as an audit that saves its report folder.
  • --output-schema gives a skill's verdict a fixed JSON shape that a script can test, while --json records the run as a JSON Lines log.
  • Drift baselines live on the machine that captured them, so schedule drift checks there rather than on a fresh CI runner.
  • Unattended live checks need network access, and in CI that runs repository code, the API key must never be a job-level environment variable.

codex exec at a glance

ItemDetails
Commandcodex exec with a task such as $seo drift compare <url>, $seo audit <url> or $blog seo-check <file>
What you give itA task in quotes, plus optional flags for the sandbox, the output file or the output format
What Codex readsYour Git repository, the skill's instructions, and the live pages or files the task names
What you get backThe final message on stdout, a saved copy with -o, a JSON Lines log with --json, or schema-shaped JSON with --output-schema
NeedsYour saved CLI sign-in or CODEX_API_KEY for one run; network access for live checks; the kits installed on that machine
Best forPost-deploy regression checks, monthly audits, and pass/fail gates for new blog posts

Why automate SEO checks?

A lot of SEO damage comes from routine deploys. A template change drops the canonical tag. A staging setting leaks noindex into production. A plugin update strips the JSON-LD. The page still looks fine to visitors, so nobody notices until traffic falls and the cause is buried under weeks of commits. A check that runs after every deploy, or every morning, reports the change while the commit behind it is still fresh.

Match the command to the question

Pick the command that answers your question:

  • Did anything break since the last good state? Use $seo drift compare. It compares a page with a stored baseline and sorts changes by severity, so it suits daily and post-deploy runs.
  • How healthy is the whole site? Use $seo audit. It crawls up to 500 pages, so it's better as a monthly job.
  • Is this post ready to publish? Use $blog seo-check on the post's file. Its pass/fail checklist fits a pre-merge gate.

Report on a schedule, fix by hand

A scheduled run should describe problems, not quietly change production. Keep unattended jobs to reporting, and make fixes in an interactive task where you read the diff. The exception is a job that exists to write a file, such as an audit saving its report folder.

How codex exec runs an SEO check

Flow diagram of an unattended codex exec run, from a cron or CI trigger through sign-in, skill loading, the sandbox and live checks to the final message your script acts on.
The seven stages of an unattended codex exec run, with the final message as the output your script acts on.
  1. A trigger fires. Cron, a CI pipeline or your own shell script starts the job.
  2. Codex signs in. codex exec reuses your saved CLI sign-in. For a single run you can supply a key with CODEX_API_KEY instead.
  3. The skill loads. The task names $seo or $blog and a command, so Codex loads that skill's full instructions.
  4. The sandbox applies. The run is read-only by default. With --sandbox workspace-write, Codex can edit files in the project.
  5. Live checks run. Commands like $seo drift compare fetch the live page, which needs network access.
  6. The final message is written. Progress streams to stderr and only the final message reaches stdout. -o also saves it to a file.
  7. Your script decides. It commits the report, compares it with the last one, sends an alert or fails the build.

codex exec expects to run inside a Git repository, which suits SEO work: commit each report, and git diff shows exactly what changed between two runs. If you must run outside a repository, --skip-git-repo-check turns that check off. OpenAI's non-interactive mode docs list every option.

Pattern 1: save a report with -o

Start with a report that saves itself, such as a monthly audit:

bash
codex exec --sandbox workspace-write '$seo audit https://example.com' -o audit-summary.md

Three details matter. First, the quotes: inside double quotes your shell reads $seo as an empty variable, so use single quotes or escape it as \$seo. Second, the sandbox: the audit writes a folder named after your domain, with the full report, the action plan, per-category findings and a JSON data file, so it needs --sandbox workspace-write. Third, -o saves Codex's closing summary to audit-summary.md and still prints it.

A task that only reads can keep the read-only default and let -o capture the result:

bash
codex exec '$blog seo-check content/blog/standing-desk-guide.md' -o seo-check-standing-desk.md

To act on a report later, codex exec resume --last continues the most recent run with a follow-up request, such as asking which commit introduced each Critical issue.

Pattern 2: catch regressions after each deploy with $seo drift

Drift monitoring answers one narrow question: what changed on this page since it was last known to be good? Capture a baseline for each important URL first, in an interactive task where you can approve anything Codex asks about:

text
$seo drift baseline https://example.com/pricing

A baseline records the title, meta description, canonical URL, robots directives, H1 to H3 headings, JSON-LD, Open Graph tags, Core Web Vitals, the HTTP status code, and hashes of the HTML and the schema. Each later $seo drift compare fetches the page again and applies 17 comparison rules across three severity levels: CRITICAL for SEO-breaking changes, WARNING for changes to investigate within a week, and INFO for changes that may be intentional.

Core Web Vitals come from PageSpeed data. If that fetch fails, for example because no API key is set up, compare skips the Core Web Vitals rules and runs the rest.

One detail decides where this job should live. Baselines are stored in a local database on the machine that captured them, outside your project files. A compare on another machine, such as a fresh CI runner, finds no baseline and asks you to capture one first. Schedule drift checks on the machine that holds the baselines. Compare also records each result in that local store, so run the scheduled command once by hand and make sure your sandbox setup allows it.

Turn the verdict into pass or fail

A report is for people. A script needs a yes or no. --output-schema asks Codex for a final answer that matches a JSON Schema, so every run returns the same shape. Save this as drift-gate.schema.json:

json
{
  "type": "object",
  "properties": {
    "url": { "type": "string" },
    "status": { "type": "string", "enum": ["pass", "fail"] },
    "critical": { "type": "array", "items": { "type": "string" } },
    "warnings": { "type": "array", "items": { "type": "string" } }
  },
  "required": ["url", "status", "critical", "warnings"],
  "additionalProperties": false
}

Then run the comparison against it and test the result:

bash
codex exec '$seo drift compare https://example.com/pricing and set status to fail if any CRITICAL rule triggered' --output-schema drift-gate.schema.json -o drift-pricing.json
jq -e '.status == "pass"' drift-pricing.json > /dev/null || echo "SEO drift on /pricing, see drift-pricing.json"

jq -e exits with an error when the test is false, so the second line can send an alert or fail a pipeline step.

Pattern 3: a pass/fail gate for new blog posts

$blog seo-check is built for the moment before you publish. It reads a post file and checks the title (40 to 60 characters, primary keyword in the first half), the meta description (150 to 160 characters), heading hierarchy, internal and external links, the canonical URL, Open Graph and Twitter Card tags, and the URL slug. Each check comes back PASS, FAIL, WARN or N/A, and the report ends with an overall verdict of PASS, NEEDS WORK or FAIL.

That verdict maps neatly onto a schema. Save this as seo-check.schema.json:

json
{
  "type": "object",
  "properties": {
    "file": { "type": "string" },
    "overall": { "type": "string", "enum": ["PASS", "NEEDS WORK", "FAIL"] },
    "failed_checks": { "type": "array", "items": { "type": "string" } }
  },
  "required": ["file", "overall", "failed_checks"],
  "additionalProperties": false
}

This script checks every post added or changed on the current branch. Change content/blog to wherever your posts live:

bash
#!/usr/bin/env bash
set -euo pipefail
mkdir -p reports/seo-check
status=0
for file in $(git diff --name-only --diff-filter=AM origin/main -- 'content/blog/*.md'); do
  name=$(basename "$file" .md)
  codex exec "\$blog seo-check $file" --output-schema seo-check.schema.json -o "reports/seo-check/$name.json"
  if ! jq -e '.overall != "FAIL"' "reports/seo-check/$name.json" > /dev/null; then
    echo "SEO check failed: $file"
    status=1
  fi
done
exit $status

The gate fails only on FAIL, so a post marked NEEDS WORK passes with its report attached for the writer to read. The read-only default fits, because the check reads the file. One part of the checklist does go online: it confirms that the post's main external links are reachable, so give the run network access if you want that check.

Keep machine-readable logs with --json

When a scheduled job fails overnight, the final message alone may not explain why. --json turns stdout into a JSON Lines event stream, one JSON object per line, recording the run as it happens:

bash
mkdir -p logs
codex exec --json '$seo drift compare https://example.com/pricing' > logs/drift-pricing.jsonl

Keep it as a record of the run, and open it when a report looks wrong. Use --json for logs and --output-schema for results your script tests. On shared machines, --ephemeral stops a run from saving session files to disk.

Grid of eight codex exec options used in SEO automation, including -o for saved reports, --json for event logs, --output-schema for pass or fail results and CODEX_API_KEY for single runs.
The codex exec options this guide uses, with --output-schema highlighted as the one that turns a report into a pass or fail result.

Run it on a schedule with cron

A shell script plus one crontab line covers most sites. Keep the logic in the script and only the timing in cron. Save this as scripts/seo-nightly.sh in your repository and make it executable:

bash
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
day=$(date +%F)
mkdir -p reports/drift
for url in https://example.com/ https://example.com/pricing https://example.com/blog/; do
  name=$(echo "$url" | sed 's#https://##; s#[/.]#-#g')
  codex exec "\$seo drift compare $url and set status to fail if any CRITICAL rule triggered" --output-schema drift-gate.schema.json -o "reports/drift/$day-$name.json"
done
git add reports/drift
git commit -m "SEO drift check $day" || true

Then add one line with crontab -e:

bash
40 6 * * * /home/you/sites/example/scripts/seo-nightly.sh >> /home/you/sites/example/reports/cron.log 2>&1

That runs the script at 6:40 every morning. Cron treats % specially, so the date lives in the script. Cron also starts with a minimal environment, so use full paths, and make sure codex is on the PATH cron sees.

codex exec reuses the CLI sign-in saved for the user who runs it, so schedule the job as the user who signed in. To supply a key for a single run instead, set it on that command only:

bash
CODEX_API_KEY="your-api-key" codex exec --sandbox workspace-write --json '$seo audit https://example.com' > logs/audit.jsonl

Set this way, the key exists for that one command, not for everything else the script or the machine runs. Load it from wherever you keep secrets on that machine, and never commit it.

Run it in CI and GitHub Actions

CI suits the $blog seo-check gate, because it runs against the files in each pull request. Keep these rules in mind:

  • Use the official action on GitHub. For GitHub Actions, OpenAI recommends the Codex GitHub Action, openai/codex-action, over installing the CLI yourself. Follow its documentation for the inputs and workflow file.
  • Keep the key out of the job environment. In CI that runs repository code, don't set CODEX_API_KEY as a job-level environment variable, where build scripts, tests or dependency install hooks in the same job could read it. Give it only to the step that runs Codex.
  • Install the kits on the runner. A runner needs the skills just like your laptop, so start with cron on a machine where the kits already work, then move gates into CI.
  • Allow the network access the task needs. Live checks need it in CI too. The Codex sandboxing docs explain the options.

--sandbox danger-full-access removes the sandbox boundaries entirely. OpenAI's docs say to use it only in a controlled environment, such as an isolated CI runner or container.

Side-by-side comparison of a risky and a safer setup for automated SEO checks, covering API keys, sandbox modes, drift baselines and shell quoting.
Six common mistakes in unattended SEO jobs, each paired with the safer setup that avoids it.

Make it repeatable with AGENTS.md

Scheduled reports are only useful if each one looks like the last. Codex reads AGENTS.md before it starts work, so a short section there keeps unattended runs consistent:

markdown
## Scheduled SEO checks
- Save reports in reports/, named YYYY-MM-DD-<page>.md or .json.
- During scheduled checks, never edit source files. Report only.
- List CRITICAL findings first, then WARNING, then INFO.
- Quote the old and the new value for every change you report.
- If a page can't be fetched, say so in the first line. Never guess its content.

Our guide to AGENTS.md rules for SEO covers the rest of the file.

Without the skill: a plain Codex prompt

You can automate a basic regression check with plain Codex and no kit:

text
Fetch https://example.com/pricing and report the HTTP status, title, meta description, canonical URL, robots meta tag, every H1, and the @type of each JSON-LD block. Compare them with reports/pricing-last.json, list anything that changed, then overwrite that file with today's values.

Run it with codex exec --sandbox workspace-write so it can update the file. What the kits add is a fixed set of captured fields, 17 comparison rules with severities instead of an improvised diff, a stored baseline history, a post checklist with set thresholds, and the same report format on every run. The skills page lists every command you can schedule, and the install section has the setup command for the machine that runs your jobs.

Pitfalls to avoid

  • Writing $seo inside unescaped double quotes. The shell swaps it for an empty variable, and Codex never sees the command.
  • Granting --sandbox workspace-write to report-only jobs. Use it only when the task should write files.
  • Running --sandbox danger-full-access on a shared machine. Keep it for isolated runners and containers.
  • Comparing drift on a machine that never captured a baseline.
  • Setting the API key as a job-level environment variable in CI that runs repository code.
  • Forgetting network access. A live check that can't reach the site produces an error, not a report.
  • Letting a schedule change production unattended. Report on a schedule, then fix in an interactive task.

Keep going

FAQ

What is codex exec?

codex exec runs Codex without its interactive interface. You pass the task as a quoted string. Progress goes to stderr and only the final message goes to stdout, so you can save it, pipe it or test it in a script.

Can codex exec edit files?

Yes, if you allow it. The default sandbox is read-only, so add --sandbox workspace-write when the task should change files in the project, such as an audit that saves its report folder.

How do I run SEO checks on a schedule?

Put the codex exec commands in a shell script, run it from your Git repository, and call it from cron or your CI scheduler. Save each result with -o and commit it, so a diff shows what changed between runs.

How do I get a pass or fail result from Codex in CI?

Use --output-schema with a JSON Schema that includes a status field, save the answer with -o, then test that field with a tool such as jq. When the test fails, your script exits with an error and the CI step fails.

Does codex exec need an API key?

Not always. It reuses your saved CLI sign-in, and for a single run you can set CODEX_API_KEY on the command itself. In CI that runs repository code, never set the key as a job-level environment variable.

Do automated SEO checks need internet access?

Yes, for anything that checks live pages, such as drift comparisons and audits. Make sure your sandbox and permission setup allows the network access the task needs. A check on a local file, like $blog seo-check on a draft, mostly reads from disk, though its external link check goes online.