from tickerq · migration notes
From TickerQ to Acta.
TickerQ and Acta agree on more than they differ, so this page starts with what does not change. Use Acta when a background job stops being a task and becomes application state: work that must survive the process that started it, and answer questions afterwards.
Shared ground
The checkboxes both have.
Source generation. AOT readiness. Persistence. A dashboard. Cron schedules. Retries. Priorities. Concurrency controls. If your comparison stops at this list, either tool serves, and TickerQ is the one you already run. None of these are why Acta exists.
The difference
What only a ledger does.
Acta's unit of durability is not the schedule entry: it is the work itself. Every job, attempt, lease, step, signal, and event is a SQL row, which is what "kill the worker, keep the work" means in practice.
- Kill the worker mid-job and completed steps do not re-run: re-entry returns their recorded outcomes.
- Wait three days for an approval without occupying a worker: the job suspends as durable state and resumes on any peer.
- Fan out children and keep lineage: a batch of a thousand items stays traceable to its parent, with results you can fetch.
- Ask a job why it is stuck: explain answers from its recorded rows, not from log archaeology.
- SELECT the evidence:
jobs_viewandevents_vieware curated operator surfaces, not storage internals.
The code, side by side
The scheduled job barely changes. The ceiling does.
public sealed class CleanupJobs
{
[TickerFunction("cleanup-expired-sessions", "*/5 * * * *")]
public Task CleanupExpiredSessions(CancellationToken ct)
{
return Task.CompletedTask;
}
}
public sealed class CleanupJobs
{
[Job("cleanup-expired-sessions")]
[JobSchedule("every-5-minutes", "5m")]
public Task CleanupExpiredSessions(CancellationToken ct)
{
return Task.CompletedTask;
}
}
The scheduled-job shape migrates almost one to one: an attribute on a method, discovered by source generation. The difference starts when a job needs more than a schedule.
public sealed record PublishRelease(string ReleaseId);
public sealed class ReleaseJobs
{
[Job("publish-release")]
public async Task Handle(PublishRelease input, JobContext ctx, CancellationToken ct)
{
// A named durable step: its outcome is recorded in SQL. Kill the worker after
// it completes and re-entry returns the stored result; the body does not run again.
var artifact = await ctx.RunStepAsync("build-artifacts",
token => BuildArtifactsAsync(input.ReleaseId, token));
// Suspends the job durably: no worker thread waits. Approve it in an hour or in
// three days, from code, the CLI, or the dashboard; it resumes on any peer worker.
var approved = await ctx.WaitSignalAsync<bool>("release-approval", ct);
if (approved)
{
await ctx.RunStepAsync("publish", token => PublishAsync(artifact, token));
}
}
}
Everything the handler has done so far, the completed build step, the pending approval, the signal once it arrives, is a row. A worker kill between any two lines loses no completed work, and the job's state is visible in SQL the whole time.
What changes
The differences you will feel in week one.
- An EF Core store becomes provider SQL with curated views. Acta's providers write a documented schema directly, and
jobs_viewandevents_vieware the supported operator surface: no entity model between you and the rows. - Chaining becomes child jobs with lineage. A handler spawns children, parent lineage is recorded, the parent can wait on results, and a fan-out of a thousand items stays traceable.
- Throttling becomes namespaces, priorities, and exclusive keys. Route work by owning service, reprioritize a live job in place, and serialize work that shares a key without blocking unrelated jobs.
- The dashboard gains a CLI. Every host binary is also the admin tool, including
jobs debug: claim a persisted job and step through its real handler under your debugger.
A fair split
Keep TickerQ if a scheduler is what you need.
The migration
Migrate one job, coexist with the rest.
There is no importer for TickerQ's tables and no attribute-compatible shim, by design. Move the one job that needs a ledger, keep TickerQ running the rest, and let the next migration argue for itself.