Automation
Creates scheduled automations the scheduler runs at the given times.
Default to a script. An automation is a deterministic script the scheduler executes. The script decides for itself whether there is anything worth saying: when nothing is up, it prints nothing and the user hears nothing; when something is up, it prints the message (that text becomes the notification). This is the key behaviour for monitoring jobs (thunderstorm alert, train-delay watch) — they must stay silent on a quiet day instead of pinging "all ok" every hour.
The LLM is a step inside the script (via the harness llm_call()), used only
when there is genuinely something to phrase or curate — never the thing that runs
every tick and decides whether to notify.
A bare natural-language --instruction (LLM every tick, always replies) still
exists for trivial one-off jobs, but is the exception, not the default.
How a job stays silent
notify modes (set via the CLI flags below):
output_only(default for--script) — deliver only when the script prints something. Empty output → nothing sent. This is what makes monitors silent.True(default for--instruction) — always deliver (empty becomes "erledigt").error— deliver only when the job fails.False— never deliver on success.
Failures (script exits non-zero / raises) are always surfaced, in every mode — a broken monitor must not fail silently.
A script reports by printing to stdout. To stay silent it simply prints nothing
(or the marker PAWLIA_SILENT).
The harness
Job scripts import pawlia.automation_harness (always importable inside a job;
the scheduler puts it on PYTHONPATH):
from pawlia.automation_harness import get_params, emit, silent, llm_call, log
get_params() -> dict # the job's --params, e.g. {"city": "Magdeburg"}
emit(text) # report text → becomes the notification (empty = silent)
silent() # say nothing (documents intent; a no-op)
log(msg) # diagnostics to stderr (never sent to the user)
llm_call(prompt, system=None, model=None) -> str # one robust LLM call, retries
Rules: do the deterministic check first; call emit() only when there is
something to report; use llm_call() sparingly; let exceptions propagate so
failures are loud.
Two patterns
A — Silent monitor (Gewitter / Bahn): gate, mostly silent, little/no LLM
#!/usr/bin/env python
from pawlia.automation_harness import get_params, emit, silent, log
import urllib.request, json
p = get_params()
city = p.get("city", "Magdeburg")
# ... fetch forecast from a weather API ...
data = json.loads(urllib.request.urlopen(f"https://api.example/forecast?q={city}").read())
if not data.get("thunderstorm"):
silent() # quiet day → nothing sent
else:
emit(f"⛈ Gewitterwarnung {city}: {data['summary']} ab {data['start']}")
B — LLM-curated digest (Morgenbericht): gather, then let the LLM shape it
#!/usr/bin/env python
import sys
from pawlia.automation_harness import get_params, emit, llm_call
messages = collect_all_messages() # deterministic data gathering
if not messages:
sys.exit(0) # nothing to report → silent
report = llm_call(
system="Du bist ein knapper Nachrichten-Kurator. Fasse zusammen, gruppiere, priorisiere.",
prompt="Erstelle einen aufgeräumten Morgenbericht aus:\n\n" + "\n".join(messages),
)
emit(report)
Building a job
Which job type? Monitor vs. one-shot
Pick the kind that matches the silence rule — before you build the script:
| Job kind | Stays silent when there is nothing to say? | Use |
|---|---|---|
| Monitor (cyclic check: thunderstorm, train delay, inbox watch) | Yes — the script decides; empty stdout = no notification | script in workspace/skills/scripts/<name>.py, register with add-job --script |
| One-shot (must produce output every tick: morning digest, daily summary) | No — the LLM is invoked every run, the response is always delivered | add-job --instruction (or a script that always emits) |
Rule of thumb: "tell me only when something is up" → script. "give me an update every morning" → instruction.
Step 1 — build the script (use skill-creator for anything non-trivial)
A trivial gate you can write inline → write it to
workspace/skills/scripts/<name>.py. Anything with API calls, parsing, or
LLM curation → ask skill-creator to build it (it knows the harness skeleton
and will test the script before you register it):
Call skill-creator with: "Baue ein Automations-Skript das [task]. Nutze das
pawlia.automation_harness (get_params/emit/silent/llm_call) und bleib still wenn
nichts zu melden ist."
The script must live under workspace/skills/scripts/ (the primary path the
scheduler resolves job scripts from; workspace/.scripts/ and automations/
still resolve as legacy fallbacks for older jobs).
Step 2 — register the job
For a monitor (script that may stay silent):
python <scripts_dir>/../organizer/scripts/organizer.py add-job \
--name "<descriptive name>" \
--schedule "<schedule>" \
--script "<name>.py" \
--params '{"city":"Magdeburg"}'
For a one-shot that always delivers output:
python <scripts_dir>/../organizer/scripts/organizer.py add-job \
--name "<name>" --schedule "<schedule>" --instruction "<instruction>"
Notify flags (override the per-kind default):
--notify-on-output— deliver only when there is output (silent on empty). Default for--script.--notify-on-error— deliver only on failure.--no-notify— never deliver on success (failures still surface).
Schedule formats:
"16:00"— daily at 16:00"interval:5m"— every 5 minutes (m/h/d)"interval:1h"— every hour"weekly:0:09:00"— weekly Monday 09:00 (0=Mon..6=Sun)"monthly:1:10:00"— monthly on the 1st at 10:00"30 8 * * 1,3"— cron syntax (5 space-separated fields)
Managing existing jobs
python <scripts_dir>/../organizer/scripts/organizer.py list-jobs
python <scripts_dir>/../organizer/scripts/organizer.py delete-job --job-id "<id>"
python <scripts_dir>/../organizer/scripts/organizer.py toggle-job --job-id "<id>"
Manual trigger (for testing)
python <scripts_dir>/../organizer/scripts/organizer.py run-job --job-id "<id>"
Flags the job for immediate execution on the next scheduler tick (within 60s). Use this after building/modifying the script to verify the job behaves: confirm the silent case sends nothing and the alert case sends exactly one message.
Working rules
- Before each bash call, output a short status line (e.g. "Registriere Job …", "Teste Script …") so the runner sees progress between steps.
- Build+Register is one continuous flow: decide type → (for monitors) build and test the script → register. Do not stop to ask for confirmation between steps.
- For monitors, always verify the silent case works after registering: run
run-joband confirm no notification is sent when the script prints nothing.
Output
After registering, confirm to the user:
- What the automation does and whether it's a script or an instruction
- How often it runs (the schedule)
- That it stays silent unless there is something to report (for monitors)
- If a skill/script was built: mention its name
- That they can list, disable, delete, or manually trigger it later