Quickstart
Build your first task queue in 5 minutes.
Build your first task queue in 5 minutes.
Four pieces make up the core loop — a task, a worker, an enqueued job, and its result — plus how to check on the queue afterward. No broker to stand up: everything below talks to one embedded store.
A task is a named function registered on the queue. Producers enqueue by name; a worker executes the registered function and stores its return value as the result.
Create a file called tasks.py:
from flexiq import Queue
# Create a queue backed by SQLite
queue = Queue(db_path="tasks.db")
@queue.task()
def add(a: int, b: int) -> int:
return a + b
@queue.task(max_retries=3, retry_backoff=2.0)
def send_email(to: str, subject: str, body: str) -> str:
# Your email sending logic here
print(f"Sending email to {to}: {subject}")
return f"sent to {to}"import { Queue } from "@byteveda/flexiq";
const queue = new Queue({ dbPath: "flexiq.db" });
// Register a task with optional per-task config.
queue.task("add", (a: number, b: number) => a + b, {
maxRetries: 3,
retryBackoff: { baseMs: 1000, maxMs: 60_000 },
timeoutMs: 30_000,
maxConcurrent: 4,
});import com.fasterxml.jackson.core.type.TypeReference;
import java.time.Duration;
import java.util.Map;
import org.byteveda.flexiq.FlexiQ;
import org.byteveda.flexiq.task.Task;
// A task is a name plus its payload type — the producer never needs the handler body.
Task<Map<String, Integer>> add =
Task.of("add", new TypeReference<Map<String, Integer>>() {})
.retries(3)
.timeout(Duration.ofSeconds(30));
FlexiQ flexiq = FlexiQ.builder().sqlite("flexiq.db").open();Add flexiq = "2" to Cargo.toml, then write src/main.rs:
use flexiq::FlexiQ;
#[flexiq::task]
fn add(a: i64, b: i64) -> flexiq::Outcome<i64> {
Ok(a + b)
}
// Durations are written the way you say them: 500ms, 30s, 5m, 2h, 1d.
#[flexiq::task(max_retries = 3, retry_backoff_ms = "2s", timeout = "30s")]
fn send_email(to: String, subject: String, body: String) -> flexiq::Outcome<String> {
// Your email sending logic here.
println!("sending to {to}: {subject} ({} bytes)", body.len());
Ok(format!("sent to {to}"))
}
fn main() -> flexiq::Result<()> {
let queue = FlexiQ::open("flexiq.db")?;
// Steps 2 and 3 go here.
Ok(())
}The name a job carries is the function name — add, not the module path —
so it is also the name a producer in any other language enqueues. The attribute
takes a fixed key set and nothing else: an unrecognised key is a compile error
that lists the ones it accepts. So are async fn, unsafe fn, a generic or
variadic signature, and a parameter written as a destructuring pattern, since
the payload is decoded into parameters one at a time.
A worker polls storage, claims due jobs, and dispatches them to the registered functions above. Start one before you enqueue — or leave it running while you enqueue in the next step:
# Runs in its own terminal and blocks, processing jobs until you stop it (Ctrl+C)
flexiq worker --app tasks:queue# Runs the worker in a background thread inside your own process
import threading
from tasks import queue
t = threading.Thread(target=queue.run_worker, daemon=True)
t.start()import asyncio
from tasks import queue
async def main():
await queue.arun_worker()
asyncio.run(main())# Loads the module that registers the tasks above and runs a worker over them
flexiq run ./app.js --queues default// Same process, or a separate one pointed at the same dbPath / DSN
const worker = queue.runWorker({ queues: ["default"] });import org.byteveda.flexiq.worker.Worker;
Worker worker = flexiq.worker()
.handle(add, p -> p.get("a") + p.get("b"))
.start();
// ... later: worker.close();// This process, or another one opened on the same file.
let worker = queue
.worker()
.register::<add>()
.register::<send_email>()
.num_workers(4)
.spawn()?;
// ... later: worker.shutdown()?;There is no worker binary to run: the worker is this process. spawn returns
as soon as its threads are up and jobs run behind main.
Nothing executes until a worker is consuming the queue. Enqueued jobs sit in
pending until a worker claims them — so leave this worker running while you
enqueue jobs in the next step.
Keeping it running means keeping the process running: the worker is this
binary, so main returning takes its threads with it. And when you do stop,
stop it deliberately — worker.shutdown() is what halts dispatch, drains
in-flight jobs and unregisters the worker. Dropping the handle does none of
that, so a binary that simply exits leaves its registration behind until another
worker's heartbeat reaps it.
From another process (or anywhere in your app), enqueue a job and read its result. The worker from step 2 picks it up and runs it:
from tasks import add
# Enqueue — returns a JobResult handle immediately; the function does NOT run here
job = add.delay(2, 3)
print(f"Job ID: {job.id}") # 01936...
print(f"Status: {job.status}") # "pending" — until a worker claims it
# Block until the worker finishes the job (exponential-backoff polling)
result = job.result(timeout=30)
print(result) # 5
# Async variant
result = await job.aresult(timeout=30)// Enqueue a job (producer).
const id = queue.enqueue("add", [2, 3], { priority: 5 });
// Await the result — the worker from step 2 picks it up and runs it.
const result = await queue.result(id); // 5
worker.stop();// Enqueue a job (producer).
String id = flexiq.enqueue(add, Map.of("a", 2, "b", 3));
// Block until the job is terminal, then read its result.
flexiq.awaitJob(id, Duration.ofSeconds(20));
int result = flexiq.getResult(id, Integer.class).orElseThrow(); // 5
worker.close();use std::time::Duration;
use flexiq::JobStatus;
// Enqueue — writes a row and comes straight back; `add` does NOT run here.
let enqueued = queue.enqueue(add::call(2, 3).priority(5))?;
println!("job {} is {:?}", enqueued.id, enqueued.status); // Pending
// Nothing blocks on a result, so poll the row until it is terminal.
let done = loop {
let job = queue.get_job(&enqueued.id)?.expect("the job exists");
if matches!(job.status, JobStatus::Complete | JobStatus::Failed) {
break job;
}
std::thread::sleep(Duration::from_millis(25));
};
println!("{:?}", done.status);
println!("{} result bytes", done.result.map_or(0, |bytes| bytes.len()));
worker.shutdown()?;result holds the encoded return value as Option<Vec<u8>> — the Rust SDK does
not wrap a typed result read.
Check queue health without leaving the terminal:
from tasks import queue
stats = queue.stats()
print(stats)
# {'pending': 0, 'running': 0, 'completed': 5, 'failed': 0, 'dead': 0, 'cancelled': 0}Or the CLI:
flexiq info --app tasks:queue# Live dashboard, refreshes every 2s
flexiq info --app tasks:queue --watchawait queue.stats(); // { pending, running, completed, failed, dead, cancelled }Or the CLI:
flexiq --db flexiq.db statsQueueStats stats = flexiq.stats(); // pending, running, completed, failed, dead, cancelledOr the CLI:
flexiq --url flexiq.db statslet stats = queue.stats()?;
println!(
"{} pending, {} completed, {} dead",
stats.pending, stats.completed, stats.dead
);This crate ships no CLI. stats() is the read, and queue.storage() reaches
the counts the handle does not wrap — stats_by_queue for one lane, list_dead
for the dead-letter queue.
For a full visual interface — job browsing, metrics charts, dead letter management, and queue controls — run the bundled dashboard. It ships in the package itself; no separate service or extra dependencies:
pnpm build:dashboard # builds the SPA into static/dashboard (one-time)
flexiq --db flexiq.db dashboard --port 8787Open http://localhost:8787 in your browser.
flexiq --url flexiq.db dashboard --port 8080 --token "$DASH_TOKEN"Open http://localhost:8080 in your browser.
The Rust SDK does not serve a dashboard. The flexiq-server crate is what
serves the bundled SPA — point its binary at the same database file.
You have run the whole loop once. What you do not have yet is the vocabulary for it — the difference between a task and a job, what "at-least-once" costs you, and why the worker is a separate process at all.
Next: Concepts — the seven words the rest of these docs assume you know, in prose and diagrams.
Not following the track? The three pages people usually want from here are Tasks (registration options), Workers (pools and graceful shutdown) and Testing.