---
title: "Schedule .NET workflows"
description: "Create cron, interval, and one-time schedules with inspectable run history."
category: ".NET SDK"
---

A schedule stores a pipeline definition plus a trigger. Every firing creates a normal pipeline, executed by the same workers and handlers as an on-demand run. SDK and server **0.5.0** are the supported pair for this guide.

## Register a schedule

This method can live in a producer application referencing `PipelogiqSDK.Core`. Supply the same connection options as the [quickstart](/docs/net/getting-started). `DispatchDueEmailsHandler` is your registered worker handler; the snippet registers a definition and does not send real email itself.

```csharp
using PipelogiqSDK.Builders;
using PipelogiqSDK.Configuration;
using PipelogiqSDK.Contracts;

static async Task<ScheduleResponse> RegisterEmailScheduleAsync(
    PipelogiqRunnerOptions options, CancellationToken ct)
{
    using var schedule = ScheduleBuilder.Create("dispatch-due-emails", options)
        .Cron("*/5 * * * *", timeZone: "Europe/Tallinn")
        .WithAction("dispatch", "DispatchDueEmailsHandler", new
        {
            BatchSize = 200,
            ScheduledFor = "{{scheduledFor}}",
            RunId = "{{runId}}"
        })
        .WithOverlapPolicy(OverlapPolicy.Skip)
        .Catchup(CatchupPolicy.None)
        .Jitter(TimeSpan.FromSeconds(10))
        .AddLabel("workflow", "email-dispatch");
    return await schedule.ApplyAsync(ct);
}
```

`ApplyAsync` is an idempotent upsert by application-scoped, case-insensitive name. Reapplying an unchanged definition does not increment its version or move the next run. Updating business configuration creates a new definition version. Omitted overlap/catchup settings preserve existing settings; specify them when application code should own them.

Stored definitions exclude the application API key, a particular run's idempotency key, and runtime traces. Keep the name stable across deployments instead of creating a new schedule every time.

## Select a trigger

| Builder call | Behavior |
|---|---|
| `.Cron("0 3 * * *", "Europe/Tallinn")` | Five-field cron in an IANA time zone. |
| `.Every(TimeSpan.FromMinutes(5))` | Fixed interval in wall-clock seconds. |
| `.RunOnceAt(DateTimeOffset.UtcNow.AddHours(1))` | One future firing, then automatic archive. |

Cron supports the documented descriptors such as `@hourly` and `@daily`; six-field expressions with seconds are not accepted. `Every` requires whole seconds, at least 10. The once instant must be in the future.

Cron is evaluated in its local time zone and stored as UTC. A time in the spring-forward gap is skipped; a repeated local time during the autumn transition can run twice. Choose UTC or a local time outside the transition hour if those semantics are unsuitable. Interval schedules do not use a time zone.

## Decide overlap and catch-up behavior

| Overlap policy | When a previous run is unfinished |
|---|---|
| `Skip` | Record a skipped tick. |
| `Allow` | Create an independent overlapping run. |
| `Replace` | Cancel the previous pipeline, then create another. |
| `Queue` | Create a pipeline depending on the in-flight run. |

`Queue` does not promise a FIFO chain: multiple queued runs can depend on the same predecessor and become eligible together. Use `Skip` when scheduled firings should not overlap. Replacement cannot reverse a side effect already performed by the cancelled run.

`CatchupPolicy.None` does not replay the missed backlog; the latest due tick may run normally. `One` creates the latest missed tick as a catch-up run. `All` replays up to the configured budget, oldest first; set it with `.Catchup(CatchupPolicy.All, max: 24)`. The SDK accepts a catch-up maximum from 1 to 1000. Inspect run history to distinguish catch-up and skipped ticks.

Jitter adds a bounded random delay to firing. It uses whole seconds from zero to 3600. The planned time remains available separately from actual firing time; cron returns to the expression instead of accumulating drift.

## Use firing-time values

The server substitutes these placeholders in stage inputs and context values:

| Placeholder | Value |
|---|---|
| `{{scheduledFor}}` | Planned tick as a UTC timestamp. |
| `{{runId}}` | Schedule run ID. |
| `{{previousPipelineId}}` | Previous pipeline ID, or an empty value on the first run. |

Other strings are not a general template language. Use the scheduled time as a business window when processing delayed batches; use an external idempotency key for each record you change.

## Inspect and control the schedule

This method lists schedules and run history using the public API:

```csharp
using PipelogiqSDK.Api;
using PipelogiqSDK.Configuration;
using PipelogiqSDK.Contracts;

static async Task InspectSchedulesAsync(
    PipelogiqRunnerOptions options, CancellationToken ct)
{
    using var client = new PipelogiqApiClient(options);
    string? cursor = null;
    do
    {
        var page = await client.ListSchedulesAsync(
            ScheduleStatus.Active, cursor: cursor, limit: 100, ct: ct);
        foreach (var schedule in page.Schedules)
            Console.WriteLine($"{schedule.Name}: next={schedule.NextRunAt}");
        cursor = page.NextCursor;
    } while (!string.IsNullOrWhiteSpace(cursor));

    var history = await client.ListScheduleRunsAsync(
        "dispatch-due-emails", limit: 20, ct: ct);
    foreach (var run in history.Runs)
        Console.WriteLine($"{run.ScheduledFor}: {run.Outcome}, pipeline={run.PipelineId}");
}
```

With that same client, name, and cancellation token:

```csharp
await client.PauseScheduleAsync(name, ct);
await client.ResumeScheduleAsync(name, ct);
var manual = await client.TriggerScheduleAsync(name, new { BatchSize = 1 }, ct);
await client.ArchiveScheduleAsync(name, ct);
```

These are separate operations, not a sequence to run blindly. Pause stops automatic firing. Resume replans from now. A manual trigger creates a run without advancing the automatic cursor and bypasses overlap/catch-up policies; its optional input replaces the first stage's input. Archive retires the schedule.

A history outcome of `Created` means a pipeline was created, not that its handlers succeeded. Inspect `PipelineId` and `PipelineStatus` for execution. `Skipped` includes a reason; `Failed` indicates creation failure. `ScheduledFor`, `FiredAt`, and `LagSeconds` show timing. Run history and schedule lists use cursors with page sizes from 1 to 200.

Public definitions redact sensitive values. Do not turn `[REDACTED]` into a replacement credential. Unchanged round trips can preserve the original secret; ambiguous edits are rejected. Prefer applying the original worker-owned definition from deployment configuration.

## Operational expectations

Schedules target ordinary application workloads with second-scale observation, not a hard real-time clock. Validate the time zones and overlap/catch-up combinations your deployment depends on. SDK fixtures establish request correctness; the recorded Lab runs also exercised actual cron, once, and interval firing, but do not establish every timing boundary or a production throughput limit.
