Skip to main content
Jobs are typed, executable units of work with strict input/output schemas, automatic retries, timeouts, permission checks, and background execution support. They are designed for operations that need reliability guarantees beyond what a simple tool call provides.
Jobs extend the FrontMCP execution model with persistent state tracking, retry logic, and DAG-based composition via Workflows.
Nx users: Scaffold with nx g @frontmcp/nx:job my-job --project my-app. See Job Generator.

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 the jobs 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:
Declare the job locally with @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:
Auto-enabled by @App({ jobs }) (issue #408). Declaring any @App({ jobs: [...] }) (or workflows: [...]) brings the jobs subsystem up with in-memory stores by default — no @FrontMcp({ jobs: { enabled: true } }) required. The SDK registers four MCP tools for job management: list_jobs, execute_job, get_job_status, and remove_job (plus parallel *_workflow tools). register_job / register_workflow are added only when jobs.allowDynamicRegistration is true. Hyphen aliases (list-jobs, execute-job, …) keep working with a deprecation log line for one release. Set @FrontMcp({ jobs }) explicitly to configure persistent storage or to opt out via jobs: { enabled: false }.

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 what execute() 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.
Security (GHSA-58v2-gpcc-jmqv, fixed in 1.7.2) — before 1.7.2 permissions was validated and stored but never evaluated, so any caller who could reach execute_job could run every job regardless of its declared rules. Servers on 1.7.1 or earlier should treat every job as reachable by every caller.

Background Execution

Jobs can run in background mode, returning a runId 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 the register_job tool (opt-in — see Dynamic registration).

Memory (Default)

Suitable for development. Data is lost on restart.

Redis

For production, configure Redis storage:
The job state and definition stores connect with every connection field of 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:
Enabling this lets any caller who can reach the tool list author and run code on the server. Leave it off unless an agent is genuinely meant to write jobs, and gate the surrounding surface with permissions when you do.
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
Don’t:
  • Use jobs for simple, synchronous operations (use tools instead)
  • Set maxAttempts too high for non-idempotent operations
  • Skip output schemas — they enable validation and type safety
  • Forget to handle the retry attempt number in your logic

Next Steps

Workflows

Compose jobs into multi-step pipelines

JobContext

Context class API reference

@Job

Decorator reference

JobRegistry

Registry API reference