Scheduled Jobs
Jobs perform scheduled application work such as expiring data, refreshing statistics and synchronizing business data. Put business logic in a Service and use the Job to call it on schedule; it shares the ready application, plugins and app.services.
Choose cron or interval for each task. Scheduling starts automatically after readiness without a separate scheduler or Worker. Only future points run: no queue, automatic retry, downtime catch-up or immediate startup execution. See the API field reference and Jobs contract.
Create your first task
Start with an existing Vext application that runs through its project scripts. Create src/jobs/heartbeat.ts:
Run the existing npm run dev script, or your build script followed by npx vext start. Plugins, services, routes and readiness finish before timers start; the first execution is the next whole-minute boundary. Framework logs include job, scheduledAt and completion duration. No task files means no scheduling resources.
Development also schedules tasks. To keep business work off development machines, merge jobs.enabled=false into the selected development profile; see configuration precedence for --config, local and provider overrides.
Discovery, exports and names
Default discovery and naming:
The six default module extensions are .ts/.js/.mjs/.cjs/.mts/.cts. Underscore filenames, declaration files and .test/.spec files are ignored; underscore directories are allowed. By default, .tsx/.jsx are not selected. Names remove the extension and trailing /index and replace slashes with dots; root index.ts becomes index. Unlike Services, names do not convert kebab-case to camelCase.
A file may export multiple definitions:
Names are billing and billing.daily. Explicit name overrides inference and allows nonempty ASCII letters, digits and _ . : -. All loaded names must be unique, including disabled definitions. Prefer stable explicit business names so file moves do not change Redis identity.
JavaScript/ESM uses the same exports; native CommonJS can use:
Runtime supports relative default/named re-exports. Static Docs/MCP resolves safe ESM re-exports within declared source roots. Wrappers, export *, dynamic CommonJS re-exports and out-of-root dependencies report incomplete evidence; an unresolved static export does not prove that no runnable task exists.
Every selected file needs a defineJob export; plain objects or helper-only files refuse startup. Keep helpers outside Jobs or use ignored filenames such as _helpers.ts. Task enabled=false still imports and validates its module/definition; avoid business side effects and independent timers at module top level. Global jobs.enabled=false skips directory discovery.
Custom directory and filters
Tasks maps to src/tasks and to tasks under the compiled root, such as dist/tasks. Do not add src/, absolute paths or ... Include replaces the defaults; exclude appends to built-in ignores. Globs are relative to the tasks directory; include=[] discovers nothing.
Globs match actual files and are never automatically rewritten after compilation. include: ["**/*.ts"] discovers source but misses emitted task.js. Use **/*.{ts,js} or other source/output pairs, or keep the defaults. MCP warns about obvious source-only globs for production, but you must still inspect build output.
cron, interval and timezone
cron expressions
Croner parses expressions; consult the installed Croner version for its extended syntax. Timezone precedence is task > jobs.timezone > UTC, requiring a valid IANA timezone. Asia/Shanghai 09:00 corresponds to UTC 01:00; convert ISO log times ending in Z accordingly.
DST creates missing or repeated local times, resolved by Croner. With the current dependency, America/New_York, 30 1 * * *, and origin 2026-11-01T05:29:59Z, the next point is 05:30Z; the following point is next day 06:30Z, not the repeated second 01:30 that day. Spring behavior also depends on the calculation origin: 30 2 * * * from 2026-03-07T08:00Z yields next day 07:30Z (local 03:30), while from 2026-03-08T07:00Z it yields next day 06:30Z. Use UTC for stable absolute times, and verify boundary dates in your chosen region rather than assuming exactly one execution each calendar day.
Fixed intervals
Interval is a positive safe integer in milliseconds, mutually exclusive with cron and task timezone. The next point is:
For interval=60000, readiness at 12:00:20 waits for 12:01:00. Readiness exactly at 12:01:00 waits for 12:02:00. There is no immediate execution or startup-relative interval. Matching replica intervals and synchronized clocks produce matching planned points.
A previously armed 12:01 point delayed to 12:01:02 may execute before the following point. If the event loop resumes at 12:02 or later, the old point is skipped and the next future point is armed. Startup/restart/recovery also selects only future points. Invalid schedules/timezones, mutually exclusive fields and active definitions without a representable future point refuse startup.
Calling Services and cooperative cancellation
This example reads remote status endpoints and updates a Service memory snapshot. The external API returns JSON {"status":"..."}; STATUS_ENDPOINTS contains comma-separated URLs. Use your project's data layer inside the Service if durable state is needed; no database API is added to Job definitions.
Run npm exec -- vext typegen as described in Service type generation to type app.services.remoteStatus, then add:
Context exposes app/name/scheduledAt/signal/logger, without req/res or a current user. scheduledAt is the planned point rather than the actual start; return values do not create execution records. HTTP routes can reuse the Service while owning request/response handling.
Checking signal only at entry does not cancel a long handler. Check each batch/loop, pass it to supported network/stream I/O and check after asynchronous steps. Unsupported drivers need their native cancellation/timeouts or smaller batches. The framework cannot interrupt code ignoring signal. Use business unique keys or transactions for important side effects; scheduling deduplication is not business idempotency.
Cluster and multiple replicas
Minimal environment configuration:
Set VEXT_REDIS_URL=redis://127.0.0.1:6379 in the startup environment or configure redis.url. Environment URL alone does not enable coordination. Precedence: valid client > url > uri > VEXT_REDIS_URL > REDIS_URL. Explicit empty URL prevents nullish fallback. See the configuration table.
Automatic namespace
Matching replicas do not need explicit namespaces. The default logical prefix is:
Standard CLI sets profile and mode. Package my-app, profile production and mode production yield vext:my-app:production:production:job:. No PID, random value or deployment directory enters the automatic identity.
Replicas need the same Redis, package/profile/mode, task names and schedules, and synchronized clocks. Override namespace only for extra isolation, such as separate businesses using the same package identity. KeyPrefix overrides the complete logical prefix and takes precedence. Unreadable package names fall back to vextjs-app; separate applications sharing that fallback need explicit isolation. Jobs does not borrow cache/Session/rate-limit connections.
Supplied client and Redis Cluster
Clients need ioredis-compatible ping() and eval(script, keyCount, ...args). Jobs owns URL connections; callers own supplied clients' options, error events and close.
Configuration is frozen before plugins run: do not mutate app.config.jobs in setup. Construct a lazy client in static configuration without opening a connection, then have a plugin obtain the final instance and own cleanup. Bootstrap providers accept JSON-like patches and cannot return client instances.
For sharded Redis Cluster, keep this lifecycle and import Cluster in configuration, replacing the client creation:
See plugins for lifecycle details. Prefer URL when manual ownership is unnecessary. Do not add an ioredis keyPrefix that transforms Jobs EVAL keys again; use jobs.redis.namespace/keyPrefix for isolation.
Initialization probes PING and EVAL; runtime scripts also require TIME, GET, SET, EXISTS, PEXPIRE and DEL with appropriate key permissions. A successful startup probe does not validate every complex ACL/TLS/Sentinel/failover scenario; test your deployment. Both keys for a task share a Redis Cluster hash slot.
Leases, markers and operations
LeaseTtl defaults to 30000ms, minimum 1000ms, renewed approximately every TTL/3. It is not business timeout and need not cover the entire handler duration. Completion releases the owner-checked lease; renewal failure requests cancellation. Redis failure skips the current point without local fallback; recovery waits for future points. Redis server time rejects premature/expired claims, so application clock skew can cause omissions.
The logical prefix differs from the actual keys:
Last stores only the most recently accepted point per task without TTL. Running is the expiring lease. Completion/crash does not delete last, preventing repeat claims for a quick task and catch-up after recovery. There is no built-in history store or cleanup command.
SCAN MATCH vext:...* misses these keys because they start with {hash}:. Verify the namespace and use a pattern including the internal tag; Redis Cluster needs scans on each primary. Avoid production KEYS.
Retired last markers may remain. Clean exact old task keys through your operations process only after all old replicas/tasks have stopped; never delete live coordination state. Changing name/namespace/prefix creates another coordination identity, so mixed old/new deployments can both trigger. Keep definitions consistent during rollout. Deleting Redis state loses deduplication evidence and cannot preserve exactly-once side effects.
Overlap, failure and shutdown
A crash after acceptance can lose an invocation. Network partitions, Redis state loss and handlers ignoring cancellation do not guarantee exactly-once business effects. Reliable delivery belongs in a separate queue module. Keep history/alerts/progress in your business storage or monitoring; return values are not persisted.
Task file changes cold-restart development workers after old timers close. Changes affecting already loaded task dependencies upgrade soft reload to cold restart and reload handlers; custom directories follow the same rule.
Documentation and MCP
Generate project Jobs documentation
Merge into your existing OpenAPI configuration:
True inherits jobs.dir/include/exclude, selecting src/tasks above. An independent Docs selection can configure {dir:"documented-tasks",include:["**/*.{ts,js}"],exclude:[]} in openapi.docs.code.jobs; explicit fields override individually and do not alter scheduling. False only disables this documentation source. Disabled definitions may appear.
Summary: JSDoc > docs.summary > docs.description > description > generated fallback. Description: JSDoc > docs.description > description. Tags merge definition tags, docs.tags and jobs, deduplicated. Re-exports retain discovery file and actual definition location.
Open /docs (or your configured path) after startup and select Jobs. Details show name, cron, interval in milliseconds, declared/effective cron timezone, switches and parse state, with schedule-field search. Unknown fields remain unknown; inferred path names cannot replace dynamic actual names, and unknown switches do not imply defaults.
Static consumers verify vextjs defineJob import/require provenance and aliases, read literal/proven immutable values, and resolve safe ESM dependencies within declared roots. Calls/wrappers, opaque spreads, mutable bindings and cyclic/out-of-root references retain partial evidence and reasons. No business modules are executed to fill gaps. Documentation coverage, runtime discovery and live execution are distinct facts.
Check with MCP
Call vext_project_inspect with section=jobs from the connected host, then vext_capability_check with capability=C34. Use vext_project_check profile=standard, configTarget=production for the production configuration, or all to compare development/production.
Results distinguish framework support, project declarations, missing evidence and startup prerequisites. Empty/disabled projects are not reported as Jobs enabled. Known Cluster-without-Redis, invalid definitions/timezones and duplicate names produce diagnostics. RuntimeVerified=false means no application startup, Redis connection or handler execution occurred. The host must then build, start and test; complete source or a Docs list does not prove execution.
Testing and acceptance
Ordinary createTestApp does not load/run src/jobs. Normal application startup under NODE_ENV=test also disables real Jobs timers. Provide explicit definitions and clock through the helper:
The helper uses real scheduling/overlap logic, without discovery or real timers. See the testing API.
Run existing project tests and build, then a real isolated startup. Observe at least one future point with scheduled job completed and planned time, then ensure shutdown prevents new triggers. Replica checks share Redis and identity. Redis validates real server time: do not use new Date(1000) for real Redis acceptance.