Quick Start
Spin up Stem in minutes with nothing but Dart installed. This walkthrough stays fully in-memory so you can focus on the core pipeline: enqueueing, retries, delays, priorities, and chaining work together.
1. Create a Demo Project
dart create stem_quickstart
cd stem_quickstart
# Add Stem as a dependency and activate the CLI.
dart pub add stem
dart pub global activate stem
Add the Dart pub cache to your PATH so the stem CLI is reachable:
export PATH="$HOME/.pub-cache/bin:$PATH"
stem --version
2. Register Tasks with Options
Replace the generated bin/stem_quickstart.dart with the script built from the
snippets below. The full, runnable version lives at
packages/stem/example/docs_snippets/lib/quick_start.dart in the repository.
Define task handlers
Each task declares its name and retry/timeout options.
- Image resize task
- Email receipt task
class ResizeImageTask extends TaskHandler<void> {
String get name => 'media.resize';
TaskOptions get options => const TaskOptions(
maxRetries: 5,
softTimeLimit: Duration(seconds: 10),
hardTimeLimit: Duration(seconds: 20),
priority: 7,
rateLimit: '20/m',
visibilityTimeout: Duration(seconds: 60),
);
Future<void> call(TaskContext context, Map<String, Object?> args) async {
final file = args['file'] as String? ?? 'unknown.png';
context.heartbeat();
print('[media.resize] resizing $file (attempt ${context.attempt})');
await Future<void>.delayed(const Duration(milliseconds: 200));
}
}
class EmailReceiptTask extends TaskHandler<void> {
String get name => 'billing.email-receipt';
TaskOptions get options => const TaskOptions(
queue: 'emails',
maxRetries: 3,
priority: 9,
);
Future<void> call(TaskContext context, Map<String, Object?> args) async {
final to = args['to'] as String? ?? 'customer@example.com';
print('[billing.email-receipt] sent to $to');
}
}
Bootstrap worker + Stem
Use StemApp to wire tasks, the in-memory broker/backend, and the worker:
- Registry + runtime bootstrap
// In-memory adapters make the quick start self-contained.
final app = await StemApp.inMemory(
tasks: [ResizeImageTask(), EmailReceiptTask()],
workerConfig: const StemWorkerConfig(
queue: 'default',
consumerName: 'quickstart-worker',
concurrency: 4,
),
);
unawaited(app.start());
final stem = app.stem;
Enqueue tasks
Publish an immediate task plus a delayed task with custom metadata:
- Immediate + delayed enqueues
final resizeId = await stem.enqueue(
'media.resize',
args: {'file': 'report.png'},
);
final emailId = await stem.enqueue(
'billing.email-receipt',
args: {'to': 'alice@example.com'},
options: const TaskOptions(priority: 10),
notBefore: DateTime.now().add(const Duration(seconds: 5)),
meta: {'orderId': 4242},
);
print('Enqueued tasks: resize=$resizeId email=$emailId');
Run the script:
dart run bin/stem_quickstart.dart
Stem handles retries, time limits, rate limiting, and priority ordering even with the in-memory adapters—great for tests and local demos.
3. Compose Work with Canvas
Stem’s canvas API lets you chain, group, or create chords of tasks. Add this helper to the bottom of the file above to try a chain:
- Canvas chain helper
Future<void> runCanvasExample(Canvas canvas) async {
final chainResult = await canvas.chain([
task(
'media.resize',
args: {'file': 'canvas.png'},
options: const TaskOptions(priority: 5),
),
task(
'billing.email-receipt',
args: {'to': 'ops@example.com'},
options: const TaskOptions(queue: 'emails'),
),
]);
print('Canvas chain complete. Final task id = ${chainResult.finalTaskId}');
}
Then call it from main once the worker has started:
- Invoke the canvas helper
final canvas = app.canvas;
await runCanvasExample(canvas);
Finally, inspect the result state before shutting down:
- Inspect task result
await Future<void>.delayed(const Duration(seconds: 6));
final resizeStatus = await app.backend.get(resizeId);
print('Resize status: ${resizeStatus?.state} (${resizeStatus?.attempt})');
await app.close();
Each step records progress in the result backend, and failures trigger retries
or DLQ placement according to TaskOptions.
4. Peek at Retries and DLQ
Force a failure to see retry behaviour:
- Simulate retry + DLQ
class EmailReceiptTask extends TaskHandler<void> {
String get name => 'billing.email-receipt';
TaskOptions get options => const TaskOptions(
queue: 'emails',
maxRetries: 3,
priority: 9,
);
Future<void> call(TaskContext context, Map<String, Object?> args) async {
final to = args['to'] as String? ?? 'customer@example.com';
if (context.attempt < 2) {
throw StateError('Simulated failure for $to');
}
print('[billing.email-receipt] delivered on attempt ${context.attempt}');
}
}
The retry pipeline and DLQ logic are built into the worker. When the task
exceeds maxRetries, the envelope moves to the DLQ; you’ll learn how to inspect
and replay those entries in the next guide.
5. Where to Next
- Connect Stem to Redis/Postgres, try broadcast routing, and run Beat in Connect to Infrastructure.
- Explore worker control commands, DLQ tooling, and OpenTelemetry export in Observe & Operate.
- Keep the script—you’ll reuse the tasks and app bootstrap in later steps.