---
title: Recurring Jobs
description: Runtime recurring schedule administration operations.
order: 63
available_in: [dotnet, nodejs]
---

Use recurring registration APIs for schedule definition.

For operator-driven schedule actions in production (Run now, Enable/Disable, Update cron), use the Observation platform Jobs page.

## Define a recurring job

:::code-group
```csharp
using DurableStack.Core.Abstractions;
using DurableStack.Hosting.DependencyInjection;

[DurableJob(Name = "daily-reconciliation", MaxAttempts = 4)]
[RecurringJob("0 2 * * *", TimeZone = "UTC", Enabled = true, AllowConcurrentRuns = false)]
public sealed class DailyReconciliationJob : IDurableJob
{
    public Task ExecuteAsync(JobContext context, CancellationToken cancellationToken = default)
        => Task.CompletedTask;
}
```

```typescript
runtime.registerRecurring(
  "daily-reconciliation",
  "0 2 * * *",
  "UTC",
  async () => {
    console.log("daily reconciliation");
  },
  {
    maxAttempts: 4,
    enabled: true,
    allowConcurrentRuns: false
  }
);
```
:::

## Jobs page actions

- Run now (ad-hoc enqueue)
- Disable or enable by schedule name
- Update cron expression and time zone

## Application-level admin service usage

For operator-driven production changes, use the Observation platform Jobs page.

Use runtime schedule-admin APIs only for internal application automation (for example, controlled workflows or integration tests):

:::code-group
```csharp
var scheduledJobs = await scheduleAdmin.ListScheduledJobsAsync(includeDisabled: true, cancellationToken);

var disabled = await scheduleAdmin.SetScheduledJobEnabledAsync(
    jobName: "heartbeat-every-minute",
    enabled: false,
    cancellationToken);

var updatedCron = await scheduleAdmin.UpdateScheduledJobCronAsync(
    jobName: "heartbeat-every-minute",
    cronExpression: "*/5 * * * *",
    timeZone: "UTC",
    cancellationToken);

var runNowQueued = await scheduleAdmin.RunScheduledJobNowAsync(
    jobName: "heartbeat-every-minute",
    cancellationToken);
```

```typescript
const scheduledJobs = await runtime.listScheduledJobs(true);

const disabled = await runtime.setScheduledJobEnabled(
  "heartbeat-every-minute",
  false
);

const updatedCron = await runtime.updateScheduledJobCron(
  "heartbeat-every-minute",
  "*/5 * * * *",
  "UTC"
);

const runNowQueued = await runtime.runScheduledJobNow("heartbeat-every-minute");
```
:::

## Operational behavior

- Disabled schedules set `nextRunAtUtc` to null until re-enabled.
- Enabling recomputes `nextRunAtUtc` from current time.
- Run-now enqueues an ad-hoc run and does not mutate cron schedule.

## Typical automation pattern

Teams that need internal automation may wrap this service behind application workflows while keeping day-to-day operator actions in the Observation UI.

:::code-group
```csharp
public sealed class ScheduleCommands
{
    private readonly IDurableScheduleAdminService _scheduleAdmin;

    public ScheduleCommands(IDurableScheduleAdminService scheduleAdmin)
    {
        _scheduleAdmin = scheduleAdmin;
    }

    public Task<bool> DisableAsync(string jobName, CancellationToken ct)
        => _scheduleAdmin.SetScheduledJobEnabledAsync(jobName, enabled: false, ct);
}
```

```typescript
export async function disableSchedule(jobName: string): Promise<boolean> {
  return runtime.setScheduledJobEnabled(jobName, false);
}
```
:::
