---
title: "Scheduling"
section: "Extensions"
framework: WebFluid
package: webfluid
version: "1.0.0b3"
stage: "beta"
released: "August 17, 2026"
canonical: "https://docs.webfluid.dev/latest/ext/scheduling.md"
html: "https://docs.webfluid.dev/latest/ext/scheduling"
audience: ai-agent
format: markdown
---

# Scheduling


`EXT_SCHEDULING` is the thinnest battery: it is not a `FluidExtension` at all, but a bare
APScheduler `AsyncIOScheduler` that the framework starts on a startup hook. Everything you know
about APScheduler applies unchanged.


## Enabling

```ini
[extensions]
EXT_SCHEDULING = 1
```

That is the whole wiring. `enable_extensions` does:

```python
if EXT_SCHEDULING: fluid.startup_hook(ext.scheduler.start)
```

The instance is created lazily on first attribute access of `webfluid.core.ext.scheduler` and
cached, so every module that imports it gets the same scheduler.

## Usage

```python
# fluid/jobs.py
from webfluid.utils.logging import factory as log


async def heartbeat():
    log.log("Beat!")
```

```python
# main.py
from webfluid import Fluid
from webfluid.core.ext import scheduler
from apscheduler.triggers.interval import IntervalTrigger

from fluid.jobs import heartbeat

scheduler.add_job(heartbeat, IntervalTrigger(seconds=5))


def prepare_fluid() -> Fluid:
    return Fluid(__name__)
```

Jobs may be added **before or after** the app is constructed — `add_job` on a non-running
`AsyncIOScheduler` queues the job and it is scheduled once `start()` runs. Both sync and async
callables work; async ones are awaited on the app's loop.

Common triggers:

```python
from apscheduler.triggers.interval import IntervalTrigger
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger

scheduler.add_job(job, IntervalTrigger(minutes=15))
scheduler.add_job(job, CronTrigger(hour=0))                  # daily at midnight
scheduler.add_job(job, CronTrigger(day_of_week="mon", hour=9))
```

## Rules

> **RULE** — A scheduled job runs without a request, so FluidContext.current() raises and url_for is None. Use FluidContext.try_current(), fluid.url_path_for() or BASE_URL. Rendering a template from a job fails on url_for unless you avoid it.

> **RULE** — Jobs run on the application's event loop. A blocking call inside one blocks every request. Wrap blocking work in webfluid.utils.run_in_executor(fn, *args).

> **WARNING** — The scheduler is in-process and has no persistent job store configured. Every process that boots runs its own copy of every job — with two workers, every job fires twice. If you scale horizontally, either run the scheduler in a single dedicated app (one app config with EXT_SCHEDULING = 1, the rest with 0) or configure a shared job store yourself before start().

## Who else uses it

Two batteries register jobs of their own, which is why they depend on this switch:

- **`EXT_JWT`** requires `EXT_SCHEDULING`: it rotates the signing secret every
  `JWT_ROTARY_INTERVAL` days and mints the first one on a startup hook.
- **`EXT_SECURITY`** adds a cleanup job that deletes `ExpiredToken` rows older than
  `SECURITY_TOKEN_MAX_AGE`, every 15 days. Without `EXT_SCHEDULING` that table grows forever.

An Additive registers its jobs from `before_enable`, which already runs inside the loop:

```python
@additive.before_enable
async def before_enable(fluid):
    from webfluid.core.ext import scheduler
    from apscheduler.triggers.cron import CronTrigger
    from .jobs import nightly

    scheduler.add_job(nightly, CronTrigger(hour=3))
```

## Next

- [`ext/sqlalchemy.md`](/latest/ext/sqlalchemy.md) — the battery most jobs need.
- [`utils/lifecycle.md`](/latest/utils/lifecycle.md) — where startup hooks fit.


---

## Navigation


- [Overview](/latest/.md) — what WebFluid is, how to navigate these docs, release state and known bugs
- [Getting Started](/latest/get-started.md) — install, minimal app, the layout the framework expects

**Configuration**
- [App configs](/latest/config/app-config.md) — `app_configs/<app>.ini`, switches, `*_FILE` secrets, `[dev]`
- [Config classes](/latest/config/config-class.md) — `register_config`, `DefaultConfig`, every framework key

**Extensions**
- [Introduction](/latest/ext/base.md) — `FluidExtension`, entry points, `expand_fluid`
- [Scheduling](/latest/ext/scheduling.md) — APScheduler `AsyncIOScheduler`
- [SQLAlchemy](/latest/ext/sqlalchemy.md) — `Model`, executors, binds, sessions
- [Migrate](/latest/ext/migrate.md) — Alembic wrapper, `prepare_fluid`, single/multi-db
- [Mail](/latest/ext/mailman.md) — SMTP, sync/async, batching, attachments
- [Babel](/latest/ext/babel.md) — gettext, domains, locale selection, formatters
- [Security](/latest/ext/security.md) — users, gates, CSRF, tokens, OAuth, bearer grants
- [Events](/latest/ext/events.md) — signals, events, queries, browser socket
- [Cache](/latest/ext/cache.md) — redis / legacy backends
- [JWTManager](/latest/ext/jwt.md) — encode/decode, rotation, revocation

**Frontend (surface)**
- [Introduction](/latest/surface/tooling.md) — feature switches, `fluid_base.html`, Tailwind, themes
- [Integration](/latest/surface/frontend.md) — `APP_FRONTEND`, htmx, Vite workspaces
- [Template resolution](/latest/surface/jinja.md) — loader order, namespaces, overriding

**Additives**
- [Introduction](/latest/additives/intro.md) — manifest, routers, enabling
- [Base Additives](/latest/additives/base.md) — `type: base`, `import_base`, extension semantics
- [Interaction between](/latest/additives/contract.md) — events/queries as contracts, requirements, packaging

**Framework utility**
- [Lifecycle](/latest/utils/lifecycle.md) — `mix()`, startup/shutdown hooks, graceful stop
- [Runtime](/latest/utils/runtime.md) — `FluidContext`, request hooks, themes, proxy, rate limits
- [Logging](/latest/utils/logging.md) — the log factory, levels, files, additive attribution

**CLI**
- [Introduction](/latest/cli/wf.md) — command tree, extension CLIs
- [Create](/latest/cli/create.md) — `wf create project/app/additive`
- [Run](/latest/cli/run.md) — `wf run`, flags, debug and interactive mode
- [Ocean](/latest/cli/ocean.md) — search, install, publish

**Reference**
- [Overview](/latest/ref.md) — package layout, stability contract
- [Core](/latest/ref/core.md) — `Fluid`, config, context, `core.ext`, constants
- [Extensions](/latest/ref/extensions.md) — the battery API surface
- [Surface](/latest/ref/surface.md) — `Frontend`, Node and Tailwind tooling
- [Additives](/latest/ref/additives.md) — `Additive`, `Router`, `Manifest`, registry helpers
- [Utils](/latest/ref/utils.md) — helpers, logging, exceptions