Jobs extend the FrontMCP execution model with persistent state tracking, retry logic, and DAG-based composition via Workflows.
Why Jobs?
Jobs fill the gap between lightweight tool calls and full workflow orchestration:
Jobs are ideal for:
- Data processing — ETL pipelines, file parsing, batch operations
- External integrations — API calls that may fail and need retries
- Long-running operations — background tasks with progress reporting
- Auditable actions — operations that need execution logs and state tracking
Creating Jobs
Class Style
Use class decorators for jobs that need dependency injection, lifecycle hooks, or complex logic:Function Style
For simpler jobs, use the functional builder:ctx is the run’s JobContext, the object a class job reaches as this: ctx.log(), ctx.progress(), ctx.get() and
ctx.attempt work the same way (up to 1.9.0 log() and progress() were protected, so only a class could call them).
Registering Jobs
Add jobs to your app via thejobs array:
Jobs from npm or Remote Servers
Job.esm() and Job.remote() exist, but startup refuses them: per-entry .esm() and .remote() loading covers tools, resources and prompts only. A jobs array that lists one fails startup with ExternalEntryNotSupportedError, naming the job and its source:
@Job() or job() instead. To run work that lives on another server, call the tool that server exposes for it with Tool.remote().
To enable the jobs system on your server, configure the top-level jobs option:
Input & Output Schemas
Jobs require both input and output schemas using Zod:Configuration
Retry Configuration
Jobs support automatic retries with exponential backoff:
The backoff schedule for defaults: 1s, 2s, 4s (capped at
maxBackoffMs).
this.attempt is the number of the attempt running: 1, then 2 on the first retry, and so on. Up to 1.9.1 it was always 1.
Results
A run’s result is whatexecute() returns, or the value it passes to this.respond(), which ends the run there. It must match the job’s outputSchema, which also strips the fields it doesn’t declare (an empty outputSchema: {} checks nothing); a result that doesn’t fails the run with INVALID_OUTPUT (output does not match outputSchema at <field>). It is not retried: execute() already ran to the end, so running it again would repeat its side effects. The same holds for a job a workflow step runs.
A job declared on an @App runs with that app’s providers (this.get(TicketStore)), as the app’s tools do, and a job declared on @FrontMcp with the server’s.
A job started with execute_job or execute_workflow runs with the caller’s request context: this.context and CONTEXT-scoped providers, including the this.remember and this.featureFlags accessors, work inside it as they do in a tool. A background run outlives the request that started it, so it gets a context of its own with the caller’s session, auth and trace, a request id of its own, and no transport. Up to 1.9.1 this.context threw RequestContextNotAvailableError in a job and CONTEXT-scoped providers threw ProviderScopedAccessError.
Up to 1.9.1 the outputSchema was not checked, this.respond() replaced the whole execute_job response with its value instead of becoming the job’s result, and a job on an @App saw only the server’s providers.
Permissions
Jobs support RBAC-style permission checks:
When no permissions are defined, the job is accessible to all authenticated users.
Once a rule targets an action, every rule for that action must pass; within a
single rule,
roles and scopes are any-of.
A directly executed job is checked in JobExecutionManager — the choke point the
execute_job tool, triggers and background runs all go through. A job reached as a
workflow step is checked against its own rules too, in WorkflowStepExecutor, so
authorizing the workflow does not launder the jobs it references. list_jobs hides a
job the caller could not run, and a denial is indistinguishable from “not found” so
the response cannot be used to enumerate restricted job names.
Roles and scopes are read from the caller’s verified token. When the server
declares authorities.claimsMapping, that mapping is authoritative; otherwise
the fallback chain is user.roles → the roles claim → the authorization’s
scopes.
Background Execution
Jobs can run in background mode, returning arunId for status polling:
Via DirectClient
Progress Reporting
Jobs can report progress and log messages during execution:
A functional job calls the same methods on its
ctx argument: ctx.log(message), await ctx.progress(pct, total, msg).
Job Stores
Jobs use two stores for persistence:State Store
Tracks execution state (JobRunRecord): run ID, state, input, result, error, logs, timing.
Definition Store
Persists dynamic job definitions registered at runtime via theregister_job tool (opt-in — see Dynamic registration).
Memory (Default)
Suitable for development. Data is lost on restart.Redis
For production, configure Redis storage:store.redis: host, port, password, db and tls, or a url (redis:// or rediss://). Beside a url, those fields fill in only what the URL leaves out, and one that contradicts it stops startup, as for the server’s redis option. Up to 1.9.2 the job stores connected with the url alone, or with host and port alone, so the password, db and tls fields were dropped, whether set on their own or beside a url; a password, database or rediss:// TLS written in the url itself was kept.
MCP Tools
When jobs are enabled, the following MCP tools are automatically registered:
Hyphen aliases (
list-jobs, execute-job, …) still resolve with a deprecation log line for one release — agents and code that hardcoded the old form keep working.
Dynamic registration is opt-in
register_job and register_workflow take a raw script string and register it
as an executable job. Since 1.7.2 they are not registered at all unless you ask
for them:
get_job_status and get_workflow_status return runs started by the calling
subject only — a run record carries the job’s inputs and results, so a guessed
runId reads as “not found”.
Best Practices
Do:- Define clear input and output schemas with
.describe()on each field - Use retries for operations that call external services
- Set appropriate timeouts based on expected execution time
- Use background mode for long-running operations
- Log meaningful progress messages for debugging
- Use jobs for simple, synchronous operations (use tools instead)
- Set
maxAttemptstoo high for non-idempotent operations - Skip output schemas — they enable validation and type safety
- Forget to handle the retry
attemptnumber in your logic
Next Steps
Workflows
Compose jobs into multi-step pipelines
JobContext
Context class API reference
@Job
Decorator reference
JobRegistry
Registry API reference