@Duty
A timer in a provider runs in every copy of a server. @Duty gives periodic work a lease, so one process on a machine or in a workspace runs it. Use it for refreshing an index or clearing old files; use a job for work a client starts and follows by run id.
@Provider()
@Duty({ name?, every, scope, leaseMs? })
class RefreshIndex {
async run({ signal }: DutyRunContext) { /* refresh while the lease is held */ }
}
Reference
@Duty(options)
Decorate a provider class with a run(context) method and register it in providers.
| Option | Type | Default | Description |
|---|---|---|---|
name | string | class name | Lease name within the server. Set it explicitly when a build renames classes. |
every | number | string | required | Positive whole milliseconds or a duration such as "30s", "5m", "1h" or "1d". At most 2147483647 ms. |
scope | "machine" | "workspace" | required | One holder per machine or per project root (nearest package.json), for this server name. |
leaseMs | number | three intervals, clamped to 3000–60000 ms | Lease lifetime; renewed every third of it. Minimum 1000 ms. |
run({ signal }) returns void or a promise. The signal aborts on lease loss or shutdown. A still-running invocation makes the next interval skip; an exception is logged. Another process can take over after the holder stops renewing its lease.
dutyMetadataOf(instance) returns its class's ResolvedDutyMetadata (name, everyMs, scope, leaseMs), or undefined when it has none. dutyMetadataSchema validates the decorator options and converts every to milliseconds.
Caveats
Leases live in owner-only ~/.frontmcp/duties.sqlite, or under FRONTMCP_HOME. They coordinate processes on one machine, not several machines. Duties need a long-lived Node process; serverless and edge deployments log a warning and do not run them. Side effects should tolerate an interrupted holder: a process that ignores an aborted signal can continue working after losing its lease.
createContainer() does not run duties unless runDuties: true and info.name are set.
Usage
Refreshing an index periodically
import { Duty, DutyRunContext, Provider } from "@frontmcp/sdk";
@Provider()
@Duty({ name: "refresh-ticket-index", every: "5m", scope: "workspace" })
export class RefreshTicketIndex {
async run({ signal }: DutyRunContext) {
if (signal.aborted) return;
// Refresh the index here, passing signal to cancellable work.
}
}Register RefreshTicketIndex in the app's providers. Every server copy registers it; the lease chooses the holder.
Checking what a duty resolves to
@Duty checks its options when the class is decorated, so a mistake fails at import, before any lease is taken. dutyMetadataOf() shows what the decorator resolved: every in milliseconds, and the lease it defaults to. This tool reads it from two duties that aren't registered, so nothing runs:
import { App, Tool, ToolContext, dutyMetadataOf } from "@frontmcp/sdk";
import { FlushAuditLog, RefreshTicketIndex } from "./duties";
@Tool({ name: "describe_duties", description: "Show how each duty's options resolve", inputSchema: {} })
class DescribeDuties extends ToolContext {
async execute() {
return { duties: [new RefreshTicketIndex(), new FlushAuditLog()].map((duty) => dutyMetadataOf(duty)) };
}
}
@App({ id: "help-desk", name: "Help Desk", tools: [DescribeDuties] })
export class HelpDesk {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
The lease defaults to three intervals, kept between 3000 and 60000 ms: 60000 for every five minutes, 6000 for every two seconds.
Troubleshooting
@Duty invalid metadata for "every"
Use positive whole milliseconds or a duration such as "30s". Intervals must fit within 2147483647 ms. The class needs a run() method; unknown options and an invalid scope or leaseMs also fail decoration with InvalidDecoratorMetadataError.
Duty "…" of server "…" is declared by two classes in this process
Give one class its own name. DutyNameConflictError prevents two implementations from sharing a lease accidentally.
Refusing to keep duty leases under …
UnsafeDutyStoreError refuses unsafe ownership or permissions. Set FRONTMCP_HOME to a folder only the server's user can write.