n4nAI

Cron jobs that call an LLM API on a schedule

Learn how to build a reliable cron job that calls an LLM API on a schedule using bash, with error handling, logging, retries, and alerting.

n4n Team3 min read614 words

Audio narration

Coming soon — every post will get a voice note here.

Running a cron job llm api schedule is the cheapest way to automate report generation, monitoring, or cleanup tasks against a language model. But naive implementations break silently when the API rate-limits or returns malformed JSON, and you won’t notice until a week of missed runs piles up.

Step 1: Choose a stable endpoint and lock down credentials

The first decision is which API surface you call. If you point a bare script at a single provider’s raw endpoint, a provider-side outage or quota exhaustion stops your job cold. Routing through a gateway that exposes an OpenAI-compatible endpoint across 240+ models gives you automatic fallback when a provider is rate-limited or degraded, which matters for unattended cron job llm api schedule calls.

Store credentials outside the script. Create a dedicated service user, then write a secrets file with restrictive permissions:

sudo useradd -r llmcron
sudo install -m 600 -o llmcron -g llmcron /dev/null /etc/llm/cron.env
sudo tee -a /etc/llm/cron.env >/dev/null <<'EOF'
LLM_API_KEY=sk-your-real-key-here
LLM_ENDPOINT=https://api.openai.com/v1/chat/completions
LLM_MODEL=gpt-4o-mini
EOF

Never embed the key in the crontab or the script. Source this file at runtime instead.

Step 2: Write a minimal bash caller

The core of any cron job llm api schedule is a script that makes one HTTP call and prints the model output. Use set -euo pipefail so a failed command or broken pipe aborts the run instead of producing empty output.

#!/usr/bin/env bash
set -euo pipefail

source /etc/llm/cron.env

PROMPT_FILE="${1:-/etc/llm/default_prompt.txt}"
if [[ ! -f "$PROMPT_FILE" ]]; then
  echo "Prompt file $PROMPT_FILE missing" >&2
  exit 1
fi

PROMPT="$(cat "$PROMPT_FILE")"

jq -n --arg model "$LLM_MODEL" --arg prompt "$PROMPT" '{
  model: $model,
  messages: [{role: "user", content: $prompt}],
  temperature: 0.2
}' > /tmp/llm_payload.json

curl -sS -f -X POST "$LLM_ENDPOINT" \
  -H "Authorization: Bearer $LLM_API_KEY" \
  -H "Content-Type: application/json" \
  -d @/tmp/llm_payload.json

This prints the raw JSON response to stdout. The -f flag makes curl exit non-zero on HTTP errors, which set -e catches.

Step 3: Add retries and backoff for transient failures

Language model endpoints throttle. A single 429 or 503 should not kill the whole job. Wrap the call in a retry loop with exponential backoff:

call_llm() {
  local attempt=1
  local max_attempts=4
  while (( attempt <= max_attempts )); do
    if RESPONSE=$(curl -sS -f -X POST "$LLM_ENDPOINT" \
      -H "Authorization: Bearer $LLM_API_KEY" \
      -H "Content-Type: application/json" \
      -d @/tmp/llm_payload.json); then
      echo "$RESPONSE"
      return 0
    fi
    echo "Attempt $attempt failed, backing off $((attempt*3))s" >&2
    sleep $((attempt*3))
    ((attempt++))
  done
  echo "All attempts failed" >&2
  return 1
}

Call call_llm instead of the bare curl. For longer schedules, add jitter to avoid thundering-herd collisions across many hosts.

Step 4: Validate the response and extract value

A 200 response can still contain an error object or truncated content. Parse with jq and fail explicitly if the expected field is absent:

RESPONSE="$(call_llm)"
CONTENT="$(echo "$RESPONSE" | jq -r '.choices[0].message.content // empty')"
if [[ -z "$CONTENT" ]]; then
  echo "Empty or malformed completion" >&2
  exit 1
fi
echo "$CONTENT"

If your gateway returns usage metadata, capture it for metering:

PROMPT_TOKENS="$(echo "$RESPONSE" | jq -r '.usage.prompt_tokens // 0')"
COMPLETION_TOKENS="$(echo "$RESPONSE" | jq -r '.usage.completion_tokens // 0')"

A gateway that provides per-token usage metering lets you append those numbers to your own cost log without extra calls.

Step 5: Log, meter, and alert

Cron output disappears unless you redirect it. Write a wrapper that timestamps every run and records token counts:

#!/usr/bin/env bash
set -euo pipefail
source /etc/llm/cron.env
LOG=/var/log/llm_cron.log
echo "$(date -u +%FT%TZ) start" >> "$LOG"

if CONTENT=$(/usr/local/bin/llm_call.sh "$@"); then
  echo "$(date -u +%FT%TZ) ok len=${#CONTENT}" >> "$LOG"
  # optionally forward to a file consumer or message queue
  echo "$CONTENT" > "/var/out/llm_$(date -u +%s).txt"
else
  echo "$(date -u +%FT%TZ) FAIL" >> "$LOG"
  # send alert to pager or webhook
  curl -sS -f -X POST "$ALERT_WEBHOOK" -d '{"text":"llm cron failed"}' || true
fi

Keep logs rotated with logrotate so they don’t fill the disk.

Step 6: Install the cron job correctly

Cron does not load your interactive shell environment. The PATH is minimal and $HOME may be unexpected. Always use absolute paths and source your env file inside the script, not in the crontab.

Edit the llmcron user’s crontab:

sudo crontab -u llmcron -e

Add a line that runs nightly at 02:15:

# m h dom mon dow command
15 2 * * * /usr/local/bin/llm_wrapper.sh /etc/llm/nightly_prompt.txt >> /var/log/llm_cron.log 2>&1

If you need a custom PATH or temporary directory, set them at the top of the wrapper script, not in cron. Test the syntax with crontab -u llmcron -l.

Step 7: Verify the cron job llm api schedule end-to-end

Do not assume the schedule works because the script runs manually as root. Switch to the service user and execute the exact command cron will run:

sudo -u llmcron /usr/local/bin/llm_wrapper.sh /etc/llm/nightly_prompt.txt

Check /var/log/llm_cron.log for the ok line and confirm the output file appears in /var/out/.

Next, force a failure to validate retry and alerting logic. Temporarily point LLM_ENDPOINT in /etc/llm/cron.env at a bad host, re-run, and watch the backoff messages appear and the FAIL log entry written. Restore the correct endpoint.

Finally, do a dry-run of the timer without waiting a day. Use faketime or simply temporarily set the cron line to * * * * * (every minute) for one cycle, then revert. You should see exactly one run per minute and no duplicate runs if the previous invocation is still in flight—if you need overlap protection, add flock around the wrapper:

15 2 * * * /usr/bin/flock -n /tmp/llm_cron.lock /usr/local/bin/llm_wrapper.sh /etc/llm/nightly_prompt.txt >> /var/log/llm_cron.log 2>&1

Your cron job llm api schedule is now resilient to throttling, logs its activity, and alerts on hard failure. The pattern scales to multiple prompts, parallel workers, and downstream pipelines without changing the core loop.

Tagsbashcronautomationscheduling

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All bash/shell scripting llm automation posts →