Scheduled Jobs
Jobs runs Cron tasks with your application, such as cleaning expired data, producing reports or synchronizing business data. A Job owns the schedule and calls a Service for business work. Starting the application discovers Jobs and waits for future scheduled points.
Jobs is enabled by default. One application process and one built-in Cluster do not need Redis. Configure shared Jobs Redis when independent applications need a common coordination domain. Jobs provides no queue, automatic retry, downtime catch-up, immediate startup execution or manual trigger (run-now).
Create your first task
Prerequisite: an existing VextJS application that starts successfully. Create:
Only cron and handler are required. No additional global Jobs configuration is needed.
- Run your existing development command, for example
npm run dev. - Wait for necessary initialization and check the discovered/enabled counts and coordination mode in Jobs logs.
- Wait for the next five-second point, then look for
Heartbeat job executedand the framework completion log.
This six-field expression includes seconds. Jobs never executes immediately at startup; scheduledAt is the planned time, while actual execution may start slightly later. HTTP listening does not prove Jobs activation or handler execution. For production, use your existing npm run build and npm start; discovery maps into the build output.
Development also runs Jobs. Use the environment switch to prevent development machines from running real business tasks.
Calling Services and cooperative cancellation
A handler can call ready app.services directly. These two files demonstrate Service integration without a database or external API:
The demonstration Service only logs a successful call; it does not delete orders. Replace it with your existing order.cleanup() implementation in a real project. The Service owns database operations, transactions and business idempotency.
Run npm exec -- vext typegen as described in Service type generation, so generated project types include app.services.order. Then create:
It calls the Service at 03:00 Shanghai time. The first run still waits for a future point. To verify integration, temporarily use */5 * * * * *, observe both business log messages, then restore the business schedule.
Checking signal once at entry only cancels work that has not started. Long handlers must keep checking between batches and asynchronous steps, and pass it to I/O that supports cancellation. For other drivers, use their native cancellation facilities. The framework cannot interrupt an uncooperative function or undo completed side effects.
Complete Job configuration
This complete definition replaces the task file above; do not append a second Job to that file. It requires the registered order Service:
leaseTtl is not a handler timeout. Redis renews the lease during execution, so a task may run longer than its TTL. A task override replaces the global Redis default; set one only when its lease policy needs to differ.
Handler context
cron and timezone
cron expressions
Supported macros: @yearly, @annually, @monthly, @weekly, @daily, @midnight, @hourly. @daily means midnight in the effective timezone. The installed Croner parses calendar extensions such as 0 0 L * * (last day of the month), 0 0 1W * * (weekday nearest the first), and 0 9 * * MON#2 (second Monday). Weekday expressions are not a public-holiday calendar.
Day-of-month and weekday use OR by default. 0 0 1 * MON matches the first of the month or Monday; 0 0 1 * +MON uses explicit + for AND. From January 2, 2026 in UTC, their next points are January 5 and June 1 respectively.
Timezone precedence is Job.timezone > jobs.timezone > UTC. Shanghai 09:00 corresponds to UTC 01:00; Z in an ISO log timestamp means UTC.
DST can remove or repeat a local time; the installed Croner determines the actual plan. With the current dependency, New York 30 1 * * * from 2026-11-01T05:29:59Z returns 05:30Z, then 06:30Z the next day, without a second 01:30 that day. For the spring gap, 30 2 * * * from 2026-03-07T08:00Z returns 07:30Z the next day (03:30 local), while a starting point of 2026-03-08T07:00Z returns 06:30Z the next day. Test the region, calculation start and boundary date for DST-sensitive business rules; choose UTC when you need fixed absolute time.
Schedule category and future points
Only Cron strings are accepted. Reject interval, Date and one-off ISO dates; there is no millisecond fixed-delay schedule. Enabled tasks need a strictly future point both when prepared and activated. A finite-year schedule stops naturally after its final point. Disabled tasks still need valid Cron syntax, but not a future point.
A minute task ready at 12:00:20 waits for 12:01:00; one ready exactly at 12:01:00 waits for 12:02:00. An already armed 12:01 point delivered at 12:01:02 may run within its window; crossing the next period skips it. Startup, restart, recovery and time spent in onReady never replay historical points.
Discovery, exports and names
Your task directory only needs real task files, for example:
Discovery recursively selects .ts/.js/.mjs/.cjs/.mts/.cts. Each selected file contains exactly one default defineJob(). Ordinary helpers, types and constants may coexist; business support modules normally live outside the task directory.
Ignore rules are separate: underscore-prefixed filenames, declaration files and .test/.spec modules are excluded. Underscore directories themselves are allowed. .tsx/.jsx are not included by default.
Explicit name wins; otherwise remove the relative task path extension and trailing /index, and replace path separators with dots. Root index.ts becomes index; order/index.ts becomes order. Kebab-case is not converted to camelCase. Names must be nonempty ASCII letters/digits or _ . : - and globally unique; disabled definitions also participate in duplicate checks.
Multiple named Jobs cannot register tasks from one file. Arrays and opaque default factories are unsupported; ordinary helper exports remain legal. JavaScript/ESM uses the same default export. CommonJS uses:
Finite relative default re-exports, aliases and precise declaration constraints are covered in the API definition rules. Put business work inside the handler, without top-level business calls or privately created timers.
Custom directory and filters
Merge this fragment into your existing src/config/default.ts:
dir is relative to src, so this selects src/tasks/** and the corresponding tasks folder under the compiled root. Do not use src/tasks, absolute paths or ... include replaces the default; exclude adds to built-in ignores. Globs match actual filenames and do not rewrite **/*.ts to production .js. Include both source/output extensions when needed. include: [] discovers nothing.
Environment switches and global configuration
A full configuration fragment can look like this; merge into existing config and normally set only values that differ from defaults:
Disable all development tasks:
Disable one task with its own enabled: false. Global disable skips discovery and imports; per-task disable still imports the module and validates definitions, TTL and names. Neither bypasses configuration validation. The normal profile merge rules apply; a development override does not automatically disable production tasks.
Cluster and multiple replicas
For example, one built-in Cluster with four Workers:
No Redis, Worker ID or internal IPC settings are required. After necessary initialization, Master checks the actual ready-set manifests and activates Jobs in each Worker. Only an activated Worker may schedule tasks. Master reports overall readiness after valid activation acknowledgements arrive; one activated Worker may register timers earlier. Resolve startup/manifest errors rather than treating Workers as independent Local schedulers.
Independent replicas sharing Redis
Multiple containers each running a built-in Cluster still count as independent applications. Merge the following into each replica when they need shared coordination:
Set VEXT_REDIS_URL in every startup environment to the same real shared service, for example redis://redis.internal:6379/0. Replace the illustrative redis.internal hostname. Loopback 127.0.0.1 is for testing on one machine, not sharing Redis across hosts. An environment URL alone does not activate coordination; jobs.redis must exist. With an environment URL, this is sufficient:
Connection precedence is client > url > uri > VEXT_REDIS_URL > REDIS_URL. Invalid clients do not fall back to a URL. An explicitly empty url does not fall through to the environment. With active tasks, a missing usable Redis target or failed initialization rejects startup.
The default namespace combines package name, VEXT_CONFIG (default default) and NODE_ENV (default production); the standard CLI supplies profile/runtime mode. The default logical prefix is vext:<namespace>:job:. Ordinary replicas do not need to repeat these settings. Different package names, profiles or modes are isolated by default. To intentionally coordinate tasks across different projects, explicitly share a namespace:
Replicas in one domain must share Redis target, effective prefix, task name, Cron, effective timezone and TTL. Moving files or changing a name/namespace/keyPrefix changes identity, so mixed deployments can run both versions. Jobs does not borrow cache, Session or rate-limit Redis configuration.
Supplied clients, Redis Cluster, ACLs and retired coordination keys are covered in advanced deployment.
Per-task lease overrides
Effective lease precedence is Job.leaseTtl > jobs.redis.leaseTtl > 30000 ms. Set leaseTtl: 60_000 in an existing definition to override the 30-second global example; other tasks keep inheriting it. Changing TTL does not require a new task name or prefix.
Execution renews the lease, so long tasks need not set TTL to their total duration. Local/IPC does not use Redis TTL but still validates it. TTL cannot force-stop a handler. shutdown.timeout is in seconds, shared across the whole application's shutdown, not a separate timeout for each Job.
Overlap, failure and shutdown
In a healthy coordination domain, a same-name task receives at most one execution qualification per scheduled point. A crash after acceptance can lose work. Quick completion or lease expiry does not make a consumed point reclaimable. IPC state does not persist across a new Master. Network partitions, lost Redis state and handlers ignoring cancellation cannot promise exactly-once side effects. Use business unique keys/transactions for idempotency; reliable delivery, history and progress belong in business storage or a separate queue.
With enabled Jobs, necessary onReady failure rejects and cleans up startup so tasks cannot use unready dependencies. With no active Jobs, existing logged isolation remains. Observational app:ready callbacks are not necessary initialization. Development changes to task files or already loaded dependencies cold-restart the application after the old scheduler closes, including custom directories.
Documentation and MCP
When project Docs is enabled, open the Jobs group at /docs. The Jobs source is enabled by default; do not add openapi.docs.code.jobs: true as a mandatory step. Selection inherits jobs.dir/include/exclude individually.
To disable only Jobs documentation, merge:
Optionally override the documentation directory without changing actual task discovery or execution eligibility:
Only documentation dir changes; include/exclude still inherit individually. Disabled definitions may appear. Dynamic unknown values remain unknown; static entries never prove startup, connectivity or execution (runtimeVerified: false).
Summary precedence is JSDoc summary, docs.summary, docs.description, description, generated fallback. Description uses JSDoc description, docs.description, description. Tags merge definition tags, docs.tags and jobs.
MCP checks and Recipes provide static evidence, require an explicit Cron when generating a task, and never run handlers or start timers. Full source fields are in the API.
Testing and acceptance
Use a controlled clock to test scheduling instead of waiting for real time:
The helper takes explicit definitions and an initial clock, runs only through tick(), and creates no discovery or real timers. Ordinary createTestApp does not load Jobs either. See the testing API.
Then verify at least one future point through real application startup. Built-in IPC requires real Master/Worker tests; independent replicas require real shared Redis tests. The helper cannot replace either. Use real scheduled time with real Redis, rather than a controlled 1970 timestamp.
Startup logs include discovered/enabled/disabled counts and mode. Execution logs include name, scheduledAt, PID/Worker, startedAt, duration, status and skipReason. nextRunAt is only the next calculated point, never a promise of successful execution. Redis clock-skew diagnostics report RTT and an offset interval without adjusting system clocks; keep replica clocks synchronized.
Troubleshooting
Continue with the API, execution contract, deployment and testing.