A headless agent runs without any user interface — no terminal, no chat window, no human in the loop. It accepts a goal at startup, executes until complete or until a stop condition fires, and writes its output to a file, database, or downstream service. This recipe shows how to build one that is reliable enough to deploy as a cron job or a serverless function.
- Scheduled reports: summarize yesterday's logs at 06:00 every morning.
- Batch enrichment: process a queue of records, calling the model once per item.
- Pipeline stages: receive a payload from an upstream service, transform it, push to the next stage.
- Async chat processing: handle messages from a queue when real-time latency is not required.
"""
headless_agent.py — accepts a goal via argv or stdin, runs to completion, exits 0 on success.
"""
import os
import sys
import json
import logging
from openai import OpenAI
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger(__name__)
client = OpenAI(
base_url="https://opendunes.com/api/v1",
api_key=os.environ["OPENDUNES_API_KEY"],
)
SYSTEM_PROMPT = """
You are an autonomous assistant. Work step by step toward the goal.
When you are done, output exactly: DONE: <result>
Do not ask clarifying questions. Make reasonable assumptions.
"""
MAX_TURNS = 30
def run(goal: str) -> str:
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": goal},
]
for turn in range(MAX_TURNS):
log.info("Turn %d/%d", turn + 1, MAX_TURNS)
response = client.chat.completions.create(
model="anthropic/claude-opus-4.8",
messages=messages,
max_tokens=2048,
)
content = response.choices[0].message.content or ""
messages.append({"role": "assistant", "content": content})
if content.startswith("DONE:"):
result = content[5:].strip()
log.info("Agent completed. Result: %s", result[:200])
return result
messages.append({"role": "user", "content": "Continue."})
raise RuntimeError(f"Agent did not complete within {MAX_TURNS} turns.")
if __name__ == "__main__":
goal = " ".join(sys.argv[1:]) or sys.stdin.read().strip()
if not goal:
print("Usage: headless_agent.py <goal>", file=sys.stderr)
sys.exit(1)
try:
result = run(goal)
print(result)
sys.exit(0)
except Exception as exc:
log.error("Agent failed: %s", exc)
sys.exit(1)
0 6 * * * OPENDUNES_API_KEY=sk-... /usr/bin/python3 /opt/agents/headless_agent.py \
"Summarize today's error logs in /var/log/app.log and write the summary to /var/log/summaries/$(date +\%F).txt"
import json
from headless_agent import run
def lambda_handler(event, context):
goal = event.get("goal", "")
if not goal:
return {"statusCode": 400, "body": "Missing goal"}
try:
result = run(goal)
return {"statusCode": 200, "body": json.dumps({"result": result})}
except Exception as exc:
return {"statusCode": 500, "body": str(exc)}
Set OPENDUNES_API_KEY as an environment variable in your function's configuration — never hardcode it.
Headless agents need robust error handling because there's no human watching:
import time
from openai import APIStatusError
def safe_completion(messages, max_retries=3):
for attempt in range(max_retries):
try:
return client.chat.completions.create(
model="anthropic/claude-opus-4.8",
messages=messages,
max_tokens=2048,
)
except APIStatusError as exc:
if exc.status_code == 429:
retry_after = int(exc.response.headers.get("retry-after", 10))
log.warning("Rate limited. Retrying in %ds", retry_after)
time.sleep(retry_after)
elif exc.status_code == 402:
raise RuntimeError("Insufficient credits. Top up your balance at https://opendunes.com/dashboard/billing") from exc
elif exc.status_code >= 500:
wait = 2 ** attempt
log.warning("Server error %d. Retrying in %ds", exc.status_code, wait)
time.sleep(wait)
else:
raise
raise RuntimeError("Exhausted retries")
Write a structured log entry after each run so you can query history:
import json, pathlib
LOG_PATH = pathlib.Path("/var/log/agents/runs.jsonl")
def log_run(goal: str, result: str | None, error: str | None, turns: int):
LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
with LOG_PATH.open("a") as f:
f.write(json.dumps({
"ts": __import__("datetime").datetime.utcnow().isoformat(),
"goal": goal[:200],
"turns": turns,
"result": (result or "")[:500],
"error": error,
}) + "\n")
The Activity Export API can complement this local log with server-side token and cost data.