Scheduler Model Pin

Pin a scheduled job to a specific model — no silent cost drift

Pin a scheduled job to a specific model so unattended runs stay on the exact model you signed off on — and fail closed the moment the default drifts.

Diagram

Model Pin

No

Yes

📌 Create with –model

💾 Snapshot on job

⏰ Fire

🔍 Drift?

✅ Run pinned

🚫 Fail closed

A scheduled job used to resolve its model at fire time, so a job created against a cheap default silently inherited whatever the default later became. Pinning snapshots the model at creation and fails closed on drift.

Quick Start

  1. Agent-first (Python)

    Snapshot the model on the ScheduleJob at creation. When model is set and pin_model is True, the run is pinned and drift fails closed.

    python
    from praisonaiagents import Agent
    from praisonaiagents.scheduler import ScheduleJob, Schedule
    
    agent = Agent(
        name="Brief",
        instructions="Give a one-paragraph daily brief.",
    )
    
    job = ScheduleJob(
        name="daily-brief",
        schedule=Schedule(kind="cron", cron_expr="0 9 * * *"),
        message="summarise the day",
        model="openai/gpt-4o-mini",   # snapshot — pinned
        pin_model=True,               # default; shown for clarity
    )
  2. Pin from the CLI

    Pass --model to capture a snapshot. The pin is on by default.

    bash
    praisonai schedule add "daily-brief" \
      -s "cron:0 9 * * *" \
      -m "summarise the day" \
      --model "openai/gpt-4o-mini"
  3. Follow the default (opt out)

    Use --no-pin for a canary or experimental job that should follow whatever the default becomes.

    bash
    praisonai schedule add "flex-brief" -s daily -m "summarise" --no-pin

How It Works

The snapshot is taken at add time; the drift check runs at fire time and branches into run-pinned or fail-closed.

Diagram
AgentExecutorScheduleStoreCLIUserAgentExecutorScheduleStoreCLIUserfire timealt[no drift][drift]schedule add –model openai/gpt-4o-minipersist job (model + pin_model snapshot)due job_check_model_drift(job, agent)pin agent.llm to snapshot, runresult (llm restored in finally)record failed, no model turnfailure summary (if deliver_on_failure)
StepWhat happens
Snapshot--model (or ScheduleJob.model) is stored on the job at creation
Drift checkNo-op when model is unset or pin_model is falsy; otherwise (provider, model) are compared after normalising a provider prefix
No driftThe run is pinned run-scoped by mutating agent.llm; a finally block restores the original llm so the pin never leaks into another turn
DriftRun is recorded failed, no model turn is taken, and a failure summary is delivered when RunPolicy(deliver_on_failure=True) is set

BotOS scheduled jobs enforce the pin too

BotOS bots that host their own scheduler (Telegram, Slack, etc.) now enforce pin_model on the _execute_schedule_job path, delegating to the same canonical drift check as the gateway executor. A pinned job whose agent has drifted from the snapshot is recorded failed with the drift reason and no LLM call is made — on both the gateway executor path and the BotOS path.

Diagram
Agent_enforce_pinned_modelBotOS._execute_schedule_jobTriggerAgent_enforce_pinned_modelBotOS._execute_schedule_jobTriggeralt[drift][no drift]due pinned jobcheck drift vs snapshotraise RuntimeErrorrecord failed (no chat call)run-scoped restore callablechat(message)resultrestore pin in finally

The finally restore is run-scoped, so the pin never leaks into the next attended turn on the shared agent instance.


Which Option?

Three levers plus the no-snapshot default cover every case.

Diagram

Unattended,
cost-sensitive

Canary /
experimental

No opinion

What kind of
scheduled job?

Pass –model
pin stays on

Pass –model –no-pin
follow the default

Don’t pass –model
pre-snapshot behaviour


Configuration

ScheduleJob fields

FieldTypeDefaultDescription
providerOptional[str]NoneModel provider snapshotted when the job was created (e.g. "openai"). Advisory metadata paired with model. None = no snapshot, no drift enforcement.
modelOptional[str]NoneModel identifier snapshotted at creation (e.g. "gpt-4o-mini"). When set and pin_model is True, the wrapper executor pins the run to this model and fails closed on drift. None preserves pre-snapshot behaviour.
pin_modelboolTrueWhen True a model snapshot is enforced. False opts into following whatever the default becomes. Only meaningful when model is set.

provider / model are persisted only when set, and pin_model is persisted only when opting out (False) — agent-only jobs stay byte-for-byte identical on disk. from_dict restores all three and is unknown-key tolerant.

ScheduleJob API Reference

Full ScheduleJob dataclass reference


Common Patterns

Pin every scheduled job in a hardened deploy

Capture a snapshot on each job so no unattended run can drift onto a costlier default.

python
from praisonaiagents.scheduler import ScheduleJob, Schedule

job = ScheduleJob(
    name="nightly-report",
    schedule=Schedule(kind="cron", cron_expr="0 2 * * *"),
    message="compile the nightly report",
    model="openai/gpt-4o-mini",
)

Fail closed and deliver the alert

Combine the pin with RunPolicy(deliver_on_failure=True) so a drift is recorded failed and surfaced to the target.

python
from praisonai.scheduler import RunPolicy

# drift → recorded failed AND delivered to the target
policy = RunPolicy(deliver_on_failure=True)

--no-pin for canary / experimental jobs

Follow whatever the default becomes for a job you want tracking the latest model.

bash
praisonai schedule add "canary" -s daily -m "smoke-test the default model" \
  --model "openai/gpt-4o-mini" --no-pin

Provider-prefix normalisation (no false drift)

A provider-qualified pin (openai/gpt-4o-mini) and a bare-model resolver (gpt-4o-mini) do not false-drift — the embedded provider prefix is lifted out before the comparison. Only the first / splits, so fine-tuned paths like openai/ft:gpt-4o:org::id keep their tail.

python
from praisonaiagents.scheduler import ScheduleJob, Schedule

# Pin carries a provider prefix; a bare-model resolution still matches.
job = ScheduleJob(
    name="brief",
    schedule=Schedule(kind="cron", cron_expr="0 9 * * *"),
    message="summarise the day",
    model="openai/gpt-4o-mini",
)

Best Practices

Pin every unattended, cost-sensitive job

An unattended run has no human to catch a costlier default. Pass --model on every scheduled job you would not want silently upgraded — the pin defaults to on, so a snapshot is all it takes.

Pair the pin with deliver_on_failure

A fail-closed drift is only useful if someone sees it. Set RunPolicy(deliver_on_failure=True) so a drift lands in your channel instead of a silent failed record.

python
from praisonai.scheduler import RunPolicy

policy = RunPolicy(deliver_on_failure=True)
Use –no-pin only for jobs meant to track the default

--no-pin opts a job into following the default forever. Reserve it for canaries and experiments — not for anything whose cost or behaviour you rely on.

Qualify the model with a provider when you have one

A provider-qualified pin like openai/gpt-4o-mini is normalised against a bare-model resolution, so it never false-drifts. Provider is only compared when both sides carry one.


Related