devops #observability #cron #ai-agents #laravel #vue #posthog #sentry #opentelemetry

Zero-Cost Observability for Agent Crons

Before buying a larger automation dashboard, prove one artifact, one alert, and one rollback path. This is a practical observability setup for small teams running AI agents, cron jobs, SaaS workflows, and content pipelines that report success without evidence.

A cron job that says success can still leave no customer email sent, no blog post published, no report attached, and no safe way back. AI-assisted development makes that failure mode sharper: the agent often completes the instruction it was given, while the system around it has no proof that the work is usable.

The fix is not a larger dashboard on day one. Before adding another paid automation layer, prove three things on the smallest possible surface: one artifact, one alert, and one rollback path. The question is not whether this demos well; it is whether it survives maintenance, handoff, and local constraints.

This post is for solo maintainers and small teams running AI agents, cron jobs, Laravel/Vue SaaS flows, content pipelines, and internal scripts that already have enough moving parts. The goal is zero-cost observability first: local files, structured logs, a shell verifier, GitHub Actions, and free developer or usage-based free tiers only when they earn their place.

Three proof checkpoints: artifact, alert, and rollback

Start With the Three Proofs

The smallest useful observability contract has three proofs.

ProofQuestion it answersCheap implementationFailure it catches
ArtifactDid the job create the thing users or maintainers need?File existence, checksum, timestamp, row count, published URLEmpty reports, missing exports, stale generated content
AlertWill a human or system know the job failed?CI failure, Sentry Cron miss, email, Slack webhook, GitHub issueSilent cron failures, hung agents, partial deploys
RollbackCan the previous known-good state be restored?Git revert, release symlink, database snapshot, content backupBroken auto-publish, bad AI edits, corrupted generated files

This is not a replacement for full observability. It is the entry ticket. The OpenTelemetry observability primer describes observability in terms of emitting signals such as traces, metrics, and logs so developers can troubleshoot without adding more instrumentation during the incident [Source: https://opentelemetry.io/docs/concepts/observability-primer/]. That definition is useful, but it also reveals the trap: teams can collect signals and still miss the artifact that mattered.

For an AI-agent cron, the artifact is usually simple:

  • a Markdown post generated under content/posts/
  • a JSON report with non-empty items
  • a CSV export with a minimum row count
  • an email body rendered and queued
  • a Laravel job record marked complete with a linked object
  • a public URL returning HTTP 200

The alert should be boring. It should fire when the proof fails, not when someone remembers to check a dashboard. Sentry documents Cron Monitoring as a way to monitor the uptime and performance of scheduled, recurring jobs [Source: https://docs.sentry.io/product/crons/]. That is useful once the job boundary is clear. First, define the boundary yourself: what output proves the job did the right work?

The rollback path is where many small systems are weakest. A cron writes a file, commits it, pushes it, and triggers a deployment. If the content is wrong, the rollback cannot depend on another AI agent guessing the repair. It needs a named command, a previous revision, or a backup directory.

The zero-cost rule
Zero-cost does not mean never paying for tools. It means the first layer should be portable, inspectable, and cheap enough to run before the team has pricing meetings. Sentry advertises a free developer plan on its pricing page [Source: https://sentry.io/pricing/]. PostHog presents transparent usage-based pricing with a free tier and says a card is only needed when usage exceeds free-tier limits or advanced needs apply [Source: https://posthog.com/pricing]. Use those tiers after the local proof exists, not instead of it.

Build the Artifact Verifier First

A verifier is a small program that refuses to accept vague success. It checks the artifact directly and exits non-zero when the output is missing, stale, empty, or suspicious.

For a content pipeline, I usually start with shell because it runs everywhere: Linux servers, GitHub Actions, local WSL, cheap VPS boxes, and most CI systems.

 1#!/usr/bin/env bash
 2set -euo pipefail
 3
 4ARTIFACT_PATH="${1:?usage: verify-artifact.sh <path> <max_age_seconds>}"
 5MAX_AGE_SECONDS="${2:-900}"
 6MIN_BYTES="${MIN_BYTES:-400}"
 7
 8if [[ ! -f "$ARTIFACT_PATH" ]]; then
 9  echo "artifact_missing path=$ARTIFACT_PATH" >&2
10  exit 20
11fi
12
13size=$(wc -c < "$ARTIFACT_PATH" | tr -d ' ')
14if (( size < MIN_BYTES )); then
15  echo "artifact_too_small path=$ARTIFACT_PATH bytes=$size min=$MIN_BYTES" >&2
16  exit 21
17fi
18
19now=$(date +%s)
20modified=$(stat -c %Y "$ARTIFACT_PATH")
21age=$(( now - modified ))
22if (( age > MAX_AGE_SECONDS )); then
23  echo "artifact_stale path=$ARTIFACT_PATH age_seconds=$age max=$MAX_AGE_SECONDS" >&2
24  exit 22
25fi
26
27sha=$(sha256sum "$ARTIFACT_PATH" | awk '{print $1}')
28echo "artifact_ok path=$ARTIFACT_PATH bytes=$size age_seconds=$age sha256=$sha"

This script does four things that a dashboard cannot infer reliably:

  1. It checks the actual file.
  2. It rejects tiny output that looks like a template failure.
  3. It rejects stale output from a previous run.
  4. It prints a checksum that can be pasted into a handoff note.

The checksum matters because AI-assisted workflows often rewrite the same filename repeatedly. When a maintainer asks, “Which draft did the agent publish?” the answer should not be “the latest one, I think.” It should be a path, timestamp, and hash.

For JSON artifacts, use a stricter verifier. This one fails if the file is invalid JSON, if required keys are missing, or if an array is empty.

 1#!/usr/bin/env python3
 2import json
 3import sys
 4from pathlib import Path
 5
 6path = Path(sys.argv[1])
 7required_key = sys.argv[2] if len(sys.argv) > 2 else "items"
 8
 9if not path.exists():
10    print(f"json_missing path={path}", file=sys.stderr)
11    sys.exit(30)
12
13try:
14    data = json.loads(path.read_text())
15except json.JSONDecodeError as exc:
16    print(f"json_invalid path={path} error={exc}", file=sys.stderr)
17    sys.exit(31)
18
19if required_key not in data:
20    print(f"json_key_missing path={path} key={required_key}", file=sys.stderr)
21    sys.exit(32)
22
23value = data[required_key]
24if isinstance(value, list) and len(value) == 0:
25    print(f"json_array_empty path={path} key={required_key}", file=sys.stderr)
26    sys.exit(33)
27
28print(f"json_ok path={path} key={required_key} type={type(value).__name__}")

Do not make this verifier clever. It should be strict, short, and easy to run at 07:15 on a Monday when the published page is wrong and the agent transcript is too long to read.

Terminal inspection panel for artifact and rollback checks

Wire the Alert Where the Job Already Runs

After the artifact verifier exists, put it in the same place the job runs. Do not start with a separate observability service. Start with the scheduler or CI lane that already owns the work.

Here is a GitHub Actions workflow for an AI-generated content draft. It runs the generation command, verifies the Markdown artifact, checks a JSON fact-check report, and uploads both as CI artifacts. If any verifier fails, the workflow fails.

 1name: content-cron-proof
 2
 3on:
 4  schedule:
 5    - cron: "0 0 * * 1-5"
 6  workflow_dispatch:
 7
 8jobs:
 9  generate-and-verify:
10    runs-on: ubuntu-latest
11    timeout-minutes: 20
12
13    steps:
14      - uses: actions/checkout@v4
15
16      - name: Generate draft
17        run: |
18          ./scripts/run-content-agent.sh \
19            --topic-file ./ops/today-topic.md \
20            --output /tmp/blog-draft.md \
21            --factcheck /tmp/factcheck.json
22
23      - name: Verify markdown artifact
24        env:
25          MIN_BYTES: "2500"
26        run: |
27          ./ops/verify-artifact.sh /tmp/blog-draft.md 1800
28
29      - name: Verify fact-check JSON
30        run: |
31          python3 ./ops/verify-json.py /tmp/factcheck.json claims
32
33      - name: Upload proofs
34        uses: actions/upload-artifact@v4
35        with:
36          name: content-cron-proof
37          path: |
38            /tmp/blog-draft.md
39            /tmp/factcheck.json

This workflow gives you one alert without buying anything: the CI job fails. It also gives you one artifact bundle: the draft and the fact-check report. That bundle is useful during handoff because the maintainer can download what the agent produced rather than reconstruct it from logs.

For server cron, the same pattern is a crontab plus a wrapper script:

1# /etc/cron.d/content-agent
2SHELL=/bin/bash
3PATH=/usr/local/bin:/usr/bin:/bin
4
515 7 * * 1-5 deploy cd /srv/zemna && ./ops/run-content-cron.sh >> /var/log/content-agent.log 2>&1

The wrapper should write a compact state file every run:

 1#!/usr/bin/env bash
 2set -euo pipefail
 3
 4STATE_DIR="/srv/zemna/var/cron-state"
 5mkdir -p "$STATE_DIR"
 6
 7run_id="$(date -u +%Y%m%dT%H%M%SZ)"
 8state="$STATE_DIR/content-agent-$run_id.json"
 9latest="$STATE_DIR/content-agent-latest.json"
10
11./scripts/run-content-agent.sh --output /tmp/blog-draft.md --factcheck /tmp/factcheck.json
12./ops/verify-artifact.sh /tmp/blog-draft.md 1800
13python3 ./ops/verify-json.py /tmp/factcheck.json claims
14
15python3 - <<'PY' > "$state"
16import json, os, time
17payload = {
18    "status": "ok",
19    "run_id": os.environ.get("run_id", "unknown"),
20    "checked_at": int(time.time()),
21    "artifact": "/tmp/blog-draft.md",
22    "factcheck": "/tmp/factcheck.json",
23}
24print(json.dumps(payload, indent=2))
25PY
26
27cp "$state" "$latest"

The example above uses an inline Python snippet for JSON writing. In a production repository, I prefer a tiny committed script so shell quoting cannot break the state file. The principle stays the same: the cron writes a proof, not just a log line.

Sentry Crons fits after this boundary is defined. It can monitor scheduled recurring jobs and alert on missed or failed check-ins [Source: https://docs.sentry.io/product/crons/]. The product pricing page lists Sentry’s free developer plan [Source: https://sentry.io/pricing/], so small teams can test the alert path without starting from an enterprise procurement path. Keep the local verifier anyway. Vendor alerts should confirm your proof, not replace it.

Source Sentry Cron Monitoring documentation

Add Product Signals Only After the Job Has Proof

Product analytics answers a different question from cron monitoring. Cron monitoring asks, “Did the scheduled work run?” Product analytics asks, “Did the user’s behavior or product state show the expected result?”

PostHog’s product analytics docs cover capturing and analyzing product events, and its docs include event capture as the base unit for reports [Source: https://posthog.com/docs/product-analytics]. PostHog’s pricing page describes transparent, usage-based pricing with a free tier [Source: https://posthog.com/pricing]. That makes it a good second layer for small teams, as long as the event is tied to a real product outcome.

A Laravel SaaS example: an AI assistant generates a weekly workspace summary. The cron artifact is the rendered summary file or database row. The product signal is whether the summary became visible to a user and whether the user opened it.

 1<?php
 2
 3namespace App\Jobs;
 4
 5use App\Models\Workspace;
 6use Illuminate\Bus\Queueable;
 7use Illuminate\Contracts\Queue\ShouldQueue;
 8use Illuminate\Support\Facades\Http;
 9use Sentry\State\Scope;
10
11class GenerateWorkspaceSummary implements ShouldQueue
12{
13    use Queueable;
14
15    public function handle(Workspace $workspace): void
16    {
17        \Sentry\configureScope(function (Scope $scope) use ($workspace): void {
18            $scope->setTag('cron.name', 'workspace-summary');
19            $scope->setTag('workspace.id', (string) $workspace->id);
20        });
21
22        $summary = app('summary.generator')->forWorkspace($workspace);
23        $path = storage_path("app/summaries/{$workspace->id}.md");
24        file_put_contents($path, $summary->markdown);
25
26        if (strlen($summary->markdown) < 800) {
27            throw new \RuntimeException('Generated summary is too small to publish safely.');
28        }
29
30        Http::withHeaders([
31            'Authorization' => 'Bearer '.config('services.posthog.key'),
32        ])->post('https://app.posthog.com/capture/', [
33            'api_key' => config('services.posthog.project_key'),
34            'event' => 'workspace summary generated',
35            'distinct_id' => 'workspace:'.$workspace->id,
36            'properties' => [
37                'workspace_id' => $workspace->id,
38                'bytes' => strlen($summary->markdown),
39                'artifact_path' => $path,
40                'generator' => 'weekly-agent-cron',
41            ],
42        ]);
43    }
44}

This code does not treat analytics as proof by itself. The proof is still the file size check and the saved artifact. The PostHog event adds a product timeline that helps later: which workspace had a generated summary, how large was it, and when did the event fire?

LayerGood questionBad question
Local verifierDoes the artifact exist and pass minimum quality gates?Can I infer success from a green dashboard?
Cron monitorDid the scheduled job start, finish, or miss its window?Can the monitor know whether my generated content is useful?
Product analyticsDid the user-facing event happen after the job?Can product events replace logs and artifacts?
Traces/logs/metricsWhere did the time, error, or dependency failure happen?Can signals compensate for no rollback plan?

OpenTelemetry’s primer frames logs, metrics, and traces as signals emitted by instrumented systems [Source: https://opentelemetry.io/docs/concepts/observability-primer/]. For small teams, those signals become valuable after the job has a stable proof contract. Without the contract, you are collecting context around an undefined result.

Layered observability stack from local verifier to telemetry signals

Keep the Rollback Path Boring

A rollback path should be executable by someone who did not write the automation. It should avoid mystery dashboards, avoid ad hoc database edits, and avoid asking an AI agent to “fix it” under pressure.

For content pipelines, I like a release directory pattern:

 1#!/usr/bin/env bash
 2set -euo pipefail
 3
 4RELEASES="/srv/zemna/releases"
 5CURRENT="/srv/zemna/current"
 6BUILD_DIR="${1:?usage: promote-release.sh <build-dir>}"
 7STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
 8TARGET="$RELEASES/$STAMP"
 9
10mkdir -p "$RELEASES"
11cp -a "$BUILD_DIR" "$TARGET"
12ln -sfn "$TARGET" "$CURRENT"
13
14echo "release_promoted target=$TARGET current=$CURRENT"

Rollback becomes one command:

 1#!/usr/bin/env bash
 2set -euo pipefail
 3
 4RELEASES="/srv/zemna/releases"
 5CURRENT="/srv/zemna/current"
 6PREVIOUS="$(find "$RELEASES" -maxdepth 1 -mindepth 1 -type d | sort | tail -n 2 | head -n 1)"
 7
 8if [[ -z "$PREVIOUS" ]]; then
 9  echo "rollback_failed reason=no_previous_release" >&2
10  exit 40
11fi
12
13ln -sfn "$PREVIOUS" "$CURRENT"
14echo "rollback_ok target=$PREVIOUS"

For Laravel, the rollback path can be as simple as “disable the schedule entry, restore the previous generated file, clear queue jobs for this class, and redeploy the previous commit.” The important part is that the command exists before the incident.

Rollbacks also need naming. rollback.sh is better than a wiki paragraph. make rollback-content is better than a Slack memory. A GitHub Actions manual workflow is better than a private terminal incantation.

For SaaS features, add a feature flag to the rollback plan. If an AI-assisted job fills a recommendation table, the UI should be able to hide that recommendation block while the backfill is repaired. Product analytics can then confirm whether users stopped seeing the broken feature, but the flag is the rollback mechanism.

Use Free Tiers With a Narrow Contract

The strongest reason to start with zero-cost observability is not saving money. It is preserving architecture discipline. A bigger dashboard can hide a weak boundary. A strict artifact verifier exposes it.

Use external tools when each one has a narrow contract:

  • Sentry Crons: scheduled job check-ins, missed runs, duration, failure alerting [Source: https://docs.sentry.io/product/crons/]
  • Sentry pricing page: a free developer plan exists for starting small [Source: https://sentry.io/pricing/]
  • PostHog product analytics: product events, reports, and behavior analysis [Source: https://posthog.com/docs/product-analytics]
  • PostHog pricing page: usage-based pricing with a free tier is published [Source: https://posthog.com/pricing]
  • OpenTelemetry: shared language for emitted signals such as traces, metrics, and logs [Source: https://opentelemetry.io/docs/concepts/observability-primer/]

That list is enough for most small pipelines. It covers job health, product behavior, and future instrumentation vocabulary without requiring a custom platform team.

The contract should fit on one screen:

 1observability_contract:
 2  job: content-agent-weekday
 3  artifact:
 4    path: /tmp/blog-draft.md
 5    verifier: ./ops/verify-artifact.sh /tmp/blog-draft.md 1800
 6    minimum_bytes: 2500
 7  alert:
 8    primary: github_actions_failure
 9    secondary: sentry_cron_checkin
10  product_signal:
11    provider: posthog
12    event: blog draft generated
13  rollback:
14    command: ./ops/rollback-content.sh
15    owner_note: "Restores previous release symlink and disables content cron if repeated failures occur."

This small YAML file has more operational value than a vague dashboard named “AI Automation Health.” It tells the maintainer what to inspect, what should alert, what event should exist, and how to go back.

YAML maintenance contract card with artifact, alert, product signal, and rollback

What You Should Do Monday Morning

Do this before buying a bigger automation dashboard.

  1. Pick one recurring job. Choose the job that creates real user or maintainer pain when it silently fails. Good candidates: content publishing, invoices, weekly reports, AI review summaries, sitemap generation, data imports, and email digests.

  2. Name the artifact. Write the exact path, table, URL, or message queue record that proves the job did its work. If you cannot name the artifact, the job is not ready for observability work. It is still design work.

  3. Write the verifier. Start with file existence, minimum size, freshness, JSON validity, row count, or HTTP 200. Keep the first version under fifty lines.

  4. Run the verifier in the same lane as the job. If the job runs in cron, call the verifier in the cron wrapper. If it runs in GitHub Actions, add a verification step. If it runs in Laravel Scheduler, make failed verification throw an exception and report it.

  5. Add one alert. CI failure is acceptable. Sentry Cron Monitoring is acceptable once the job boundary is named. A Slack webhook is acceptable if it fires only on proof failure.

  6. Write the rollback command. Do not write “manually restore previous version.” Write the command. Test it on a non-production path.

  7. Add one product event only if it answers a user-facing question. Use PostHog or another analytics tool to mark a visible product outcome, not to compensate for missing artifact proof.

  8. Make the proof easy to hand off. Store the latest state file, artifact hash, CI artifact bundle, and rollback instruction where another maintainer can find them.

By Friday, you should be able to answer four questions without reading an agent transcript:

  • What did the job create?
  • Who or what alerts when the proof fails?
  • What product event confirms the user-facing result?
  • What command gets us back to the previous known-good state?

If those answers exist, then a dashboard has something real to display. If they do not exist, a dashboard only changes the shape of the uncertainty.

Further reading

A small team does not need to observe everything on Monday. It needs one artifact, one alert, and one rollback path that actually work. Start there. Everything else can earn its place.