Zmplr.Jobs 2026.2.1

dotnet add package Zmplr.Jobs --version 2026.2.1
                    
NuGet\Install-Package Zmplr.Jobs -Version 2026.2.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Zmplr.Jobs" Version="2026.2.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Zmplr.Jobs" Version="2026.2.1" />
                    
Directory.Packages.props
<PackageReference Include="Zmplr.Jobs" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Zmplr.Jobs --version 2026.2.1
                    
#r "nuget: Zmplr.Jobs, 2026.2.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Zmplr.Jobs@2026.2.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Zmplr.Jobs&version=2026.2.1
                    
Install as a Cake Addin
#tool nuget:?package=Zmplr.Jobs&version=2026.2.1
                    
Install as a Cake Tool

Zmplr.Jobs

Zmplr.Jobs is a lightweight job scheduler for .NET. Register typed handlers at application startup, run them once, on a cron schedule, or only when triggered, and observe them through a stable HTTP DTO API under /zmplr.

Host TFM /zmplr surface
ASP.NET Core net9.0 MVC controllers (MapControllers)
Classic ASP.NET / IIS (OWIN) net48 app.UseZmplrJobs(services)

Job definitions and schedules stay in-process (no durable queue / crash recovery). Run results are persisted to a JSON history file so the API and monitor UI can show history across restarts.

It is deliberately small. If you need durable queues, multi-server workers, or schedule recovery after downtime, use Hangfire, Quartz.NET, or a message bus instead.


What it does

  • Startup registration — declare jobs when the host starts via AddZmplrJobs
  • Typed handlers — implement IJobHandler or IJobHandler<TArgs>; each run executes inside a DI scope
  • One-time jobs — run once after the application has started (optional)
  • Recurring jobs — 5-field cron schedules via NCrontab
  • Manual jobs — never auto-scheduled; run via HTTP trigger or IZmplr.Enqueue (in-memory FIFO, one at a time)
  • Runtime enqueueIZmplr.Enqueue(...) works for any registered job kind (queues if busy); optional per-run arguments
  • Single-flight / serial execution — at most one concurrent run per job (enqueue queues extras)
  • Persisted run history — status, timing, exception message, and args JSON per run, stored in zmplr-history.json (matched by job name after restart)
  • HTTP monitor API — list jobs, inspect runs, trigger a job (optional JSON body args), cancel a running/pending work item; optional Bearer auth via host policies
  • Host-native engineBackgroundService polling on a one-second tick after ApplicationStarted

Mental model

Concept Meaning
Job Named definition registered at startup (handler type, kind, optional cron)
Work item One execution of a job (status, start/stop, error message, optional args JSON)
Job engine Hosted service that starts due jobs and accepts manual triggers
Handler Your IJobHandler / IJobHandler<TArgs> resolved from DI for that run

Jobs are definitions. Work items are runs. The monitor API exposes both as plain DTOs so out-of-process UIs (or scripts) can watch the process without taking a dependency on internal types.


What it does and does not do

Zmplr is intentionally not a replacement for full job platforms. Compared with Hangfire, Quartz.NET, Azure Functions, or similar:

Capability Zmplr.Jobs Typical alternatives
Durable job queue / schedule recovery No — jobs are re-declared at startup; missed ticks are not replayed SQL/Redis job stores
Run-result history for API/UI Yes — JSON file by job name (default on) Often part of the job store
Runtime enqueue Yes — in-process IZmplr.Enqueue with per-job FIFO (not durable across restarts) Hangfire, queues
Crash recovery of in-flight / pending work No — stale Running/Pending rows become Failed on next start Common
Retries with backoff No built-in retry policy Common
Distributed / multi-instance workers No — one process Clustering, distributed locks
Change schedules via API No — cron is fixed at registration Often yes
Expression / lambda job registration No — handlers are types implementing IJobHandler / IJobHandler<TArgs> Hangfire expressions, etc.
Built-in dashboard UI No — only the /zmplr JSON API (optional external monitor) Hangfire dashboard, etc.
Authentication on monitor endpoints Opt-in — host configures JWT (etc.) and RequireAuthorization policies Often packaged or documented
Continuations, batches, recurring with calendar complexity No Richer platforms

Use Zmplr when you want simple background work inside a single process (ASP.NET Core or classic ASP.NET via Generic Host + OWIN), typed handlers, cron or trigger-on-demand, runtime enqueue with serial queues, ops-visible run history, and a small HTTP surface for tooling.

Do not use Zmplr when pending work must survive restarts, run across multiple instances, or require delivery guarantees beyond the current process.


Requirements

  • net9.0 — ASP.NET Core host; package references Microsoft.AspNetCore.App and registers /zmplr controllers. Call MapControllers() (or equivalent). Optional RequireAuthorization uses host JWT policies.
  • net48 — .NET Framework 4.8 Generic Host (Microsoft.Extensions.Hosting) plus OWIN for /zmplr. Start with ZmplrHost.Start(...) from Application_Start, stop with ZmplrHost.Stop() from Application_End, and call app.UseZmplrJobs(ZmplrHost.Services) in OWIN Startup. Optional RequireAuthorization uses host-registered IZmplrAuthorizationService (e.g. AuthenticatedUserZmplrAuthorizationService for Windows auth).

Install

dotnet add package Zmplr.Jobs

Quick start

1. Implement a handler

public sealed class HeartbeatHandler : IJobHandler
{
    public Task ExecuteAsync(JobContext context, CancellationToken cancellationToken)
    {
        // context.JobId, context.JobName, context.WorkItemId
        return Task.CompletedTask;
    }
}

public sealed record NotifyOrderArgs(Guid OrderId, string Email);

public sealed class NotifyOrderHandler : IJobHandler<NotifyOrderArgs>
{
    public Task ExecuteAsync(JobContext context, NotifyOrderArgs args, CancellationToken cancellationToken)
    {
        // args from Enqueue / HTTP trigger body
        return Task.CompletedTask;
    }
}

2. Register jobs at startup

builder.Services.AddZmplrJobs(jobs =>
{
    // Runs once after the app has started
    jobs.AddOneTime<StartupPingHandler>("startup-ping", group: "system");

    // Every minute (5-field cron: minute hour day-of-month month day-of-week)
    jobs.AddRecurring<HeartbeatHandler>("heartbeat", "* * * * *", group: "system");

    // Never auto-scheduled; trigger or IZmplr.Enqueue (in-memory FIFO)
    jobs.AddManual<SendEmailHandler>("send-email", group: "demo");
    jobs.AddManual<NotifyOrderHandler>("notify-order", group: "demo");
});

Classic ASP.NET / IIS (net48)

// OWIN Startup.Configuration
ZmplrHost.Start(
    jobs =>
    {
        jobs.AddRecurring<HeartbeatHandler>("heartbeat", "*/5 * * * *", group: "system");
        jobs.RequireAuthorization(readPolicy: "Zmplr.Read", writePolicy: "Zmplr.Write");
    },
    HttpRuntime.AppDomainAppPath,
    services => services.AddSingleton<IZmplrAuthorizationService, AuthenticatedUserZmplrAuthorizationService>());

app.UseZmplrJobs(ZmplrHost.Services);

// Global.asax Application_End
ZmplrHost.Stop();

Point the standalone monitor at the site base URL (e.g. http://localhost:48000). /zmplr includes basic CORS for browser clients. When RequireAuthorization is set, register an IZmplrAuthorizationService in the host (see Authentication and authorization).

3. Enqueue from application code

public class OrdersController(IZmplr zmplr) : ControllerBase
{
    [HttpPost("{id}/notify")]
    public IActionResult Notify(Guid id)
    {
        var workItemId = zmplr.Enqueue<NotifyOrderHandler, NotifyOrderArgs>(
            new NotifyOrderArgs(id, "customer@example.com"));
        // or: zmplr.Enqueue("notify-order", new { orderId = id, email = "customer@example.com" });
        return Accepted(new { workItemId });
    }
}

If the job is already running, further enqueues become Pending and run in order when the current run finishes. Pending items are not recovered after process restart.

4. Map controllers

var app = builder.Build();
app.MapControllers();
app.Run();

AddZmplrJobs registers the job repository, hosted JobEngine, IZmplr, and adds the package’s controller assembly as an application part. Your host still owns middleware, CORS, and auth (see Authentication and authorization).


Job registration API

services.AddZmplrJobs(jobs =>
{
    jobs.AddOneTime<THandler>(string name, bool runOnStart = true, string group = null);
    jobs.AddManual<THandler>(string name, string group = null);
    jobs.AddRecurring<THandler>(string name, string cronExpression, string group = null);
    jobs.HistoryFile("zmplr-history.json");               // default; under content root
    // jobs.DisableHistory();                             // in-memory only for this process
    // jobs.RequireAuthorization(readPolicy: "Zmplr.Read", writePolicy: "Zmplr.Write");
});
Method Behavior
AddOneTime Kind OneTime. With runOnStart: true (default), becomes eligible once after startup. After that run, it is not auto-scheduled again (you can still trigger it).
AddManual Kind Manual. Never auto-scheduled. HTTP trigger / IZmplr.Enqueue start a run; additional enqueues wait in an in-memory FIFO queue.
AddRecurring Kind Recurring. Uses NCrontab. After each run — success or failure — the next occurrence is calculated. IZmplr.Enqueue can also queue extra runs.
group Optional string on every registration method; exposed as JobDto.Group for monitor grouping. Null/blank means ungrouped.
HistoryFile JSON path file name under the host content root for run results (default zmplr-history.json). Keeps the newest 100 runs per job name.
DisableHistory Turns off disk persistence; runs remain visible only until the process exits.
RequireAuthorization Opt-in policies for /zmplr. readPolicy applies to GET endpoints; writePolicy to trigger/cancel. Null policies leave those endpoints anonymous. On ASP.NET Core the host configures JWT policies + middleware; on net48 register IZmplrAuthorizationService (see Authentication and authorization).

Handlers are registered with TryAddScoped<THandler>(). Each execution creates an async DI scope and resolves the handler from that scope. THandler must implement IJobHandler or IJobHandler<TArgs>.

Inject IZmplr to enqueue:

Call Behavior
Enqueue(name) Queue a run with no args
Enqueue(name, args) Queue a run; args is JSON-serialized onto the work item
Enqueue<THandler>(name?) Ensure manual job exists, then enqueue
Enqueue<THandler, TArgs>(args, name?) Ensure manual job exists, enqueue with typed args
TryEnqueue(...) Same as above without throwing on missing job

Returns the work item id (Pending until it starts). Typed handlers receive deserialized args (or default when none were supplied).

History is keyed by job name (registration names must stay stable). Job GUIDs still change every process start; after restart, past runs are re-attached to the newly created job with the same name. In-flight Running rows from a crashed process are loaded as Failed with a ProcessTerminated message.

Cron format

Five fields only, as understood by NCrontab:

* * * * *
│ │ │ │ │
│ │ │ │ └── day of week (0–6, Sunday = 0)
│ │ │ └──── month (1–12)
│ │ └────── day of month (1–31)
│ └──────── hour (0–23)
└────────── minute (0–59)

Examples: * * * * * (every minute), 0 */6 * * * (every 6 hours on the hour), 30 2 * * 1 (02:30 on Mondays).

Invalid expressions throw at host startup when jobs are materialized.


Execution behavior

  1. The engine waits until IHostApplicationLifetime.ApplicationStarted.
  2. Every ~1 second it asks each job TimeToRun() and starts eligible jobs that are not already running.
  3. For a run it creates a work item, opens a DI scope, resolves the handler, and calls ExecuteAsync with a linked cancellation token (host shutdown + work-item cancel).
  4. Outcomes:
    • Success → work item Succeeded
    • OperationCanceledExceptionCanceled
    • Other exceptions → Failed, ErrorMessage set to ExceptionType: Message, then logged by the engine
  5. Recurring jobs always reschedule after a run (including failures). One-time jobs clear NextRun after a run.

Trigger (POST /zmplr/jobs/{id}/trigger) starts a job immediately, bypassing schedule eligibility, but still enforces single-flight: if the job is already running you get 409 Conflict. An optional JSON body is stored as that run’s args (and passed to IJobHandler<TArgs>).

Cancel (POST /zmplr/workitems/{id}/cancel) cancels the work item’s token. Your handler must honor cancellationToken for cancel to have effect. Terminal work items (Succeeded / Failed / Canceled) return 409.


HTTP monitor API

Base route: /zmplr. Authentication is opt-in via RequireAuthorization. Without policies, endpoints are anonymous — protect them in the host if exposed.

Method Path Auth policy (when configured) Description
GET /zmplr/jobs Read All jobs (DTO projection, ordered by name)
GET /zmplr/jobs/{id} Read Single job
POST /zmplr/jobs/{id}/trigger Write Start now (202 Accepted, or 404 / 409); optional JSON body = args
GET /zmplr/workitems Read All work items; optional ?jobId={guid}
GET /zmplr/workitems/{id} Read Single work item
POST /zmplr/workitems/{id}/cancel Write Request cancel (200, or 404 / 409)

Job DTO (shape)

Each job includes:

  • Identity: id, name, group, kind, cronExpression, nextRun (UTC)
  • Live state: isRunning, lastStatus, lastErrorMessage
  • runs — up to the 50 most recent work items for that job, each with id, status, startTime (UTC), stopTime (UTC), errorMessage, args (JSON text or null)
  • argsSchema — when the handler is IJobHandler<TArgs>, a list of { name, type } fields (string | number | boolean | guid | datetime | json) for monitor UIs; empty when the job takes no args

Failed runs keep their exception summary on the individual run, not only on the last error field.

Timestamps (nextRun, work-item startTime / stopTime) are stored and serialized as UTC (DateTimeKind.Utc). Cron schedules are still evaluated in the job Clock time zone; only the resulting nextRun instant is converted to UTC.

JSON uses ASP.NET Core defaults from your host (enum string converters, etc., if you configure them).


Authentication and authorization

Zmplr does not ship a login UI, token issuer, or identity provider. Calling RequireAuthorization tells Zmplr which policy names to apply to /zmplr actions.

Host How policies are enforced
ASP.NET Core (net9) Host configures authentication (typically JWT Bearer) and authorization policies. Zmplr applies standard [Authorize] filters.
Classic ASP.NET / OWIN (net48) Host registers IZmplrAuthorizationService in DI (e.g. AuthenticatedUserZmplrAuthorizationService). OWIN /zmplr calls it when policy names are set. Missing service with policies configured → 500.

In-process IZmplr.Enqueue(...) is not HTTP and is not covered by these policies — only the monitor API is.

What gets protected

Policy slot Endpoints
Read (ReadPolicy) GET /zmplr/jobs, GET /zmplr/jobs/{id}, GET /zmplr/workitems, GET /zmplr/workitems/{id}
Write (WritePolicy) POST /zmplr/jobs/{id}/trigger, POST /zmplr/workitems/{id}/cancel
  • Both policies unset (default) → all /zmplr endpoints stay anonymous.
  • Only WritePolicy set → GETs stay open; trigger/cancel require auth that satisfies the write policy.
  • Only ReadPolicy set → GETs require auth; trigger/cancel stay anonymous (unusual; prefer setting write as well).
  • Both set → full protection for the monitor API.

Failed auth returns 401 Unauthorized (no/invalid identity) or 403 Forbidden (authenticated but policy failed). OPTIONS (CORS preflight) is not authorized.

Classic ASP.NET / OWIN (net48)

On net48, Zmplr does not plug into ASP.NET’s [Authorize] pipeline. Authorization is three optional host hooks in DI; the OWIN /zmplr surface decides what to do based on which ones you register.

Host registers What Zmplr does
IZmplrAuthorizationService (required if RequireAuthorization is set) For each protected /zmplr request, call Authorize(principal, policyName, …). Missing service with policies configured → 500.
IZmplrBearerTokenValidator (optional) If the request has Authorization: Bearer …, turn the token into an IPrincipal before authorization.
IZmplrAuthenticateService (optional) Enable Zmplr’s built-in login endpoint at the fixed path POST /zmplr/authentication/authenticate.

How the principal is established is up to the host: IIS/Windows auth, Bearer tokens (via the validator), or anything else already on HttpContext. You only need the authenticate service if you want that specific /zmplr login route.

POST /zmplr/authentication/authenticate

This path is defined by Zmplr, not by your application’s routing. You do not map or configure the URL yourself. When UseZmplrJobs runs:

  • If IZmplrAuthenticateService is registeredPOST /zmplr/authentication/authenticate accepts { "username", "password" }, calls your Authenticate(...), and returns a JSON string token (or 401 on failure).
  • If it is not registered — the same path responds 404 (authenticate_not_configured).

Your class can be named anything (ZmplrJwtAuthService, etc.); only the interface registration matters. Token issuance logic lives in your implementation; Zmplr only wires the HTTP endpoint to it.

Authorization does not depend on this endpoint. Clients can obtain a Bearer token from your own API, an IdP, or paste one into the monitor — as long as IZmplrBearerTokenValidator / IZmplrAuthorizationService accept the resulting principal.

Example: IIS / Windows authentication
ZmplrHost.Start(
    jobs =>
    {
        // ...register jobs...
        jobs.RequireAuthorization(readPolicy: "Zmplr.Read", writePolicy: "Zmplr.Write");
    },
    HttpRuntime.AppDomainAppPath,
    services => services.AddSingleton<IZmplrAuthorizationService, AuthenticatedUserZmplrAuthorizationService>());

AuthenticatedUserZmplrAuthorizationService allows any authenticated IPrincipal (policy names are labels only). Implement IZmplrAuthorizationService yourself to map policy names to roles, AD groups, or other rules. No authenticate or Bearer hooks needed when IIS already authenticates the user.

Example: Bearer tokens (+ optional Zmplr login endpoint)
ZmplrHost.Start(
    jobs =>
    {
        // ...register jobs...
        jobs.RequireAuthorization(readPolicy: "Zmplr.Read", writePolicy: "Zmplr.Write");
    },
    HttpRuntime.AppDomainAppPath,
    services =>
    {
        services.AddSingleton<MyJwtAuth>(); // host-owned JWT mint/validate + policy checks
        services.AddSingleton<IZmplrBearerTokenValidator>(sp => sp.GetRequiredService<MyJwtAuth>());
        services.AddSingleton<IZmplrAuthorizationService>(sp => sp.GetRequiredService<MyJwtAuth>());
        // Optional: enables POST /zmplr/authentication/authenticate → MyJwtAuth.Authenticate
        services.AddSingleton<IZmplrAuthenticateService>(sp => sp.GetRequiredService<MyJwtAuth>());
    });

With the authenticate registration above, the monitor (or any client) can use auth URL /zmplr/authentication/authenticate. Without it, point the monitor at another login URL you own, or paste a Bearer token.

ASP.NET Core — JWT Bearer

Step 1 — Add JWT Bearer packages
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

(Your host already references the shared ASP.NET Core framework; this package wires the JWT Bearer handler.)

Step 2 — Configure authentication and policies
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

var jwt = builder.Configuration.GetSection("Jwt");
var signingKey = jwt["SigningKey"]
    ?? throw new InvalidOperationException("Configure Jwt:SigningKey.");

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = jwt["Issuer"],
            ValidateAudience = true,
            ValidAudience = jwt["Audience"],
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(signingKey)),
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromMinutes(1)
        };
    });

builder.Services.AddAuthorization(options =>
{
    // Example claim model: claim type "zmplr" with value "read" and/or "write".
    // A write token should also satisfy read (list + trigger with one token).
    options.AddPolicy("Zmplr.Read", policy =>
        policy.RequireAuthenticatedUser()
            .RequireClaim("zmplr", "read", "write"));

    options.AddPolicy("Zmplr.Write", policy =>
        policy.RequireAuthenticatedUser()
            .RequireClaim("zmplr", "write"));
});

Example appsettings.json (dev symmetric key — use secrets / Key Vault in production):

{
  "Jwt": {
    "Issuer": "my-app",
    "Audience": "zmplr-api",
    "SigningKey": "replace-with-a-long-random-secret-at-least-32-chars"
  }
}

You can instead validate tokens from Azure AD, Auth0, IdentityServer, etc. by configuring AddJwtBearer (or OpenId Connect) against that authority. Policy requirements then use whatever claims your IdP issues (roles, scope, permissions, …).

Step 3 — Opt Zmplr into those policies
builder.Services.AddZmplrJobs(jobs =>
{
    jobs.RequireAuthorization(
        readPolicy: "Zmplr.Read",
        writePolicy: "Zmplr.Write");

    // or:
    // jobs.RequireAuthorization(o =>
    // {
    //     o.ReadPolicy = "Zmplr.Read";
    //     o.WritePolicy = "Zmplr.Write";
    // });

    jobs.AddManual<NotifyOrderHandler>("notify-order");
    // … other jobs …
});

Policy names must match what you registered in AddAuthorization. Zmplr only stores the names and applies AuthorizeFilter to the matching actions.

Step 4 — Enable the auth middleware

Order matters: authentication and authorization must run before endpoints are mapped.

var app = builder.Build();

app.UseCors(/* allow your monitor origin; include Authorization via AllowAnyHeader or explicit */);
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();
app.Run();

If you use the optional monitor SPA from another origin, CORS must allow the Authorization header (e.g. AllowAnyHeader() or .WithHeaders("Authorization", "Content-Type")).

Step 5 — Call /zmplr with a Bearer token
GET /zmplr/jobs HTTP/1.1
Host: localhost:5170
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
curl -s http://localhost:5170/zmplr/jobs \
  -H "Authorization: Bearer $TOKEN"

curl -s -X POST http://localhost:5170/zmplr/jobs/{job-id}/trigger \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"orderId":"...","email":"ops@example.com"}'

Common setups

Write-only protection (GETs public on a private network)

jobs.RequireAuthorization(writePolicy: "Zmplr.Write");

Full API locked down (recommended when /zmplr is reachable from browsers or the internet)

jobs.RequireAuthorization(readPolicy: "Zmplr.Read", writePolicy: "Zmplr.Write");

Roles instead of a custom claim

options.AddPolicy("Zmplr.Write", p =>
    p.RequireAuthenticatedUser().RequireRole("JobsAdmin"));

Scopes from an OIDC access token (claim type often "scope" or "scp" depending on IdP):

options.AddPolicy("Zmplr.Write", p =>
    p.RequireAuthenticatedUser()
     .RequireAssertion(ctx =>
         ctx.User.FindAll("scope").Any(c => c.Value.Split(' ').Contains("zmplr.write"))
         || ctx.User.FindAll("scp").Any(c => c.Value == "zmplr.write")));

What stays unauthenticated

Surface Protected by RequireAuthorization?
/zmplr/* HTTP API Yes, when policies are set
IZmplr.Enqueue / handlers inside the process No — same trust boundary as your app code
Your own controllers / minimal APIs No — configure separately

Do not expose an unauthenticated token minting endpoint in production. The sample host’s POST /demo/token (username/password → JSON access_token, or a host may return a plain token string) is for local demos only.

Monitor UI

The optional zmplr.monitor.app stores a per-source auth URL and Bearer token. Use Auth → username/password → Sign in & save token (posts { username, password }, accepts a plain token string or JSON access_token), or paste a token manually. List and trigger requests then send Authorization: Bearer ….

On a net48 host that registered IZmplrAuthenticateService, set the auth URL to /zmplr/authentication/authenticate (Zmplr’s built-in endpoint). Otherwise use any other login URL your app exposes (e.g. ExampleWeb’s POST /demo/token).

Working sample

Zmplr.Jobs.ExampleWeb wires JWT Bearer, Zmplr.Read / Zmplr.Write, RequireAuthorization, middleware, CORS, and POST /demo/token with demo credentials (demo / demo). See that project’s README for curl examples.


Comparison cheat sheet

Hangfire — persistent storage, dashboard, runtime enqueue, retries, multi-server. Heavier operational footprint. Choose Hangfire when durability and dynamic enqueue matter.

Quartz.NET — mature scheduling, calendars, clustering with a job store. Choose Quartz when you need rich scheduling semantics and persistence.

Coravel / IHostedService timers — fine for a few app-local tasks with little ops surface. Zmplr sits nearby but adds named jobs, cron, trigger/cancel, and a DTO monitor API aimed at external monitors.

Message queues (Azure Service Bus, RabbitMQ, …) — choose these for cross-service work, competing consumers, and delivery guarantees. Zmplr is in-process orchestration, not a bus.


Design choices (and trade-offs)

Schedules in memory, results on disk
Job definitions and due times are not recovered from storage — you re-register them at startup. Completed runs are written to a JSON history file so monitors can still show history after a restart, without adopting a full job-store stack.

Handlers over expressions
Jobs are IJobHandler / IJobHandler<TArgs> types registered in DI. You get constructor injection and testability without serializing lambdas. Per-run arguments are JSON on the work item.

Register at launch
The set of jobs is part of application composition. There is no API to invent new job types at runtime. Trigger/cancel operate on already registered jobs.

Stable DTO API
/zmplr returns records suitable for third-party monitors. An example UI is available from the GitHub repo (see Monitor UI); it is not shipped inside this package.

Recurring jobs keep going after failure
A failed run is recorded on the work item; the next cron occurrence is still scheduled. There is no automatic retry-with-backoff of the same due time.


Monitor UI

The React monitor is available in two forms:

Package / folder Role
Zmplr.Jobs.Monitor NuGet Optional embed: AddZmplrMonitor() + MapZmplrMonitor() (default /zmplr-ui, configurable PathPrefix)
zmplr.monitor.app Standalone Vite app (same UI; source for the NuGet build)

Both support multi-host API sources (empty base URL = this origin; add remotes by absolute URL).

Embedded in your app

builder.Services.AddZmplrMonitor();
// …
app.MapZmplrMonitor();

See Zmplr.Jobs.Monitor/README.md.

Standalone

  1. Run a host with Zmplr.Jobs (e.g. ExampleWeb on http://localhost:5170).
  2. Allow CORS from the monitor origin if the UI is on another origin.
  3. In zmplr.monitor.app: npm install then npm run dev.
  4. Open http://localhost:3000; sign in against each protected host as needed.

Full instructions: zmplr.monitor.app/README.md


Package contents

  • Core scheduler types and JobEngine hosted service (net9.0 and net48)
  • DI extensions: AddZmplrJobs / ZmplrJobsBuilder (including RequireAuthorization)
  • Typed handlers: IJobHandler and IJobHandler<TArgs> with per-run args on work items / DTOs
  • JSON run-history store (zmplr-history.json by default)
  • ASP.NET Core MVC controllers for /zmplr (application part)
  • Classic ASP.NET / OWIN /zmplr surface (ZmplrHost, UseZmplrJobs, optional auth interfaces)

Not included: identity provider / token issuer. Optional UI: separate package Zmplr.Jobs.Monitor (ASP.NET Core host).

Project home: mtanneryd.github.io/zmplr
Source and samples: github.com/mtanneryd/zmplr-jobs


Versioning

Package versions are produced with GitVersion (same scheme as other Zmplr libraries):

Branch Example
master / main 2026.2.0
develop 2026.2.0-beta.…
feature/* 2026.2.0-alpha.…
release/* 2026.2.0-rc.N

Release notes

2026.2.0
  • Multi-target net9.0 + net48 — same scheduler engine on ASP.NET Core and classic ASP.NET / IIS
  • OWIN /zmplr APIZmplrHost + UseZmplrJobs for Generic Host under IIS
  • net48 authorization — host-supplied IZmplrAuthorizationService (Windows-auth helper included); optional IZmplrBearerTokenValidator; optional IZmplrAuthenticateService enables fixed POST /zmplr/authentication/authenticate
  • CORS — basic CORS on the OWIN /zmplr surface for browser monitors
  • DI — fixed multiple-constructor resolution for JobController
  • Package metadata — project website points at the Zmplr home page; repository URL is this source repo
2026.1.0
  • Optional Zmplr.Jobs.Monitor embedded multi-host UI
  • Opt-in JWT authorization for /zmplr on ASP.NET Core
  • Typed handlers with per-run args, runtime enqueue, JSON run history

License

Licensed under the Apache License, Version 2.0.

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Framework net48 is compatible.  net481 was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2026.2.1 75 8/12/2026
2026.2.0 74 8/12/2026
2026.1.0 87 8/9/2026
2026.1.0-beta.20260806.27766 58 8/6/2026
2026.1.0-beta.20260805.79770 49 8/6/2026
2026.1.0-beta.20260805.79017 57 8/5/2026
2026.1.0-beta.20260805.74119 50 8/5/2026