Skip to content

Workflow and Background Jobs

František Bereň edited this page Sep 30, 2026 · 3 revisions

Workflow and Background Jobs

Two separate things that often appear together: jobs run work later, workflows track what state something is in.


Background jobs

Enqueue work now, run it elsewhere.

using Birko.BackgroundJobs;
using Birko.BackgroundJobs.Processing;
using Birko.Time;

var queue = new InMemoryJobQueue(new SystemDateTimeProvider());

var jobId = await queue.EnqueueAsync(new JobDescriptor { JobType = nameof(SendEmailJob) });

// a worker picks it up
var job = await queue.DequeueAsync();
await new SendEmailJob().ExecuteAsync(new JobContext(job!.Guid, 1, job.EnqueuedAt));
await queue.CompleteAsync(jobId);

A job is one method:

public class SendEmailJob : IJob
{
    public Task ExecuteAsync(JobContext context, CancellationToken cancellationToken = default)
    {
        // do the work
        return Task.CompletedTask;
    }
}

Where the queue lives

InMemoryJobQueue is for tests and single-process apps. For anything that must survive a restart, pick a backend — the API above does not change:

SQL · MongoDB · RavenDB · ElasticSearch · CosmosDB · Redis · JSON · XML

var queue = new SqlJobQueue(settings, new SystemDateTimeProvider());

Retries and scheduling

  • RetryPolicy on the descriptor controls attempts and backoff
  • JobScheduler runs jobs on a schedule
  • JobProcessor is the loop that dequeues and executes — host it in a BackgroundService

Workflows

A state machine with guards and actions, for things that move through defined states — an order, an approval, a document.

using Birko.Workflow.Definition;
using Birko.Workflow.Execution;

var workflow = new WorkflowBuilder<Order>("Orders")
    .InitialState("New")
    .State("New").And()
    .State("Paid").And()
    .State("Shipped").IsFinal().And()
    .Transition("pay",  "New",  "Paid").And()
    .Transition("ship", "Paid", "Shipped").And()
    .Build();

var instance = WorkflowInstance<Order>.Create(workflow, new Order());

var result = await new WorkflowEngine().FireAsync(workflow, instance, "pay");

if (result.IsSuccess)
    Console.WriteLine(instance.CurrentState);   // "Paid"

Firing a trigger that isn't valid from the current state fails rather than moving — check result.IsSuccess.

Guards and actions

A transition can be conditional, and can do something on the way through:

  • guard — may this transition happen, given the data?
  • action — run this as it happens

Persisting instances

IWorkflowInstanceStore keeps instances between requests, with backends for SQL, MongoDB, RavenDB, ElasticSearch, CosmosDB, JSON, XML.

⚠️ All workflows share one table or collection per backend, so every query takes the workflow name. Saving an instance under a different name than it was stored with is refused rather than silently relabelling somebody else's row.

Diagrams

A workflow can export itself as Mermaid or DOT, which is a cheap way to review a state machine with someone who does not read C#.

Next

Clone this wiki locally