Appearance
Enqueue Standalone Jobs
How to enqueue single background jobs without a workflow graph.
Prerequisites
Use a cyclone.Client with your handlers registered. See Installation and Migrations.
What Are Standalone Jobs?
A standalone job is a one-node run. It has no DAG and no upstream dependency. It uses the same execution model, lease recovery, retry machinery, and observability as a workflow step. Use a standalone job for independent background work. Send a notification, process a file, or sync a record.
Enqueue a Step
go
runID, err := cyclone.Enqueue(ctx, client, FetchStep, FetchInput{URL: "https://example.com"})
if err != nil {
return fmt.Errorf("enqueue: %w", err)
}
fmt.Println("job started:", runID)Enqueue opens its own transaction. Enqueue inserts the run and the step. Enqueue commits. It then publishes a NOTIFY to wake sleeping workers. The publish is best-effort. Polling is the fallback. The returned runID identifies the run.
Enqueue an Action
go
runID, err := cyclone.EnqueueAction(ctx, client, SendEmailAction, EmailInput{
To: "user@example.com",
Subject: "Your report is ready",
})Enqueue Inside a Business Transaction
Use EnqueueTx when the standalone job must exist only if your business write commits:
go
tx, err := pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
var orderID int
if err := tx.QueryRow(ctx,
`INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id`,
userID, total,
).Scan(&orderID); err != nil {
return err
}
runID, err := cyclone.EnqueueTx(ctx, client, tx, ProcessOrderStep,
ProcessOrderInput{OrderID: orderID},
)
if err != nil {
return err
}
return tx.Commit(ctx)See Transactional Enqueueing for the full treatment.
Enqueue with a Specific Queue
go
cyclone.Enqueue(ctx, client, ReportStep, in, cyclone.WithQueue("bulk"))See Queues and Priority.
Enqueue with Priority
go
// High priority — claimed before default-priority jobs
cyclone.Enqueue(ctx, client, UrgentStep, in, cyclone.WithPriority(10))Enqueue with a Delay
go
// Process in 10 minutes
cyclone.Enqueue(ctx, client, RetryWebhookStep, in,
cyclone.WithProcessIn(10*time.Minute),
)
// Process at a specific time
midnight := time.Now().UTC().Truncate(24*time.Hour).Add(24*time.Hour)
cyclone.Enqueue(ctx, client, NightlyReportStep, in,
cyclone.WithProcessAt(midnight),
)Cyclone inserts the step in the available state with a future scheduled_at. Workers skip it until the time arrives.
Enqueue with a Per-Attempt Timeout
go
cyclone.Enqueue(ctx, client, ImportStep, in,
cyclone.WithTimeout(5*time.Minute),
)See Timeouts, Cancellation, and Leases.
Enqueue with Custom Retry Behavior
go
// Override max attempts for this specific enqueue:
cyclone.Enqueue(ctx, client, WebhookStep, in,
cyclone.WithMaxAttempts(3),
)Or set defaults at the step definition:
go
var WebhookStep = cyclone.NewStep("webhook", webhookFn,
cyclone.WithMaxAttempts(5),
cyclone.WithBackoff(func(attempt int) time.Duration {
return time.Duration(attempt) * 30 * time.Second
}),
)Enqueue with Retention
go
cyclone.Enqueue(ctx, client, QuickStep, in,
cyclone.WithRetention(24*time.Hour), // delete terminal run after 24h
)Enqueue with Uniqueness
go
runID, err := cyclone.Enqueue(ctx, client, SyncUserStep, SyncInput{UserID: 99},
cyclone.WithUnique("user:99"),
)
if err != nil {
var dup *cyclone.DuplicateError
if errors.As(err, &dup) {
fmt.Printf("already syncing as %s\n", dup.RunID)
return nil
}
return err
}See Unique Jobs.
Inspect Run Status
go
run, err := client.GetRun(ctx, runID)
if err != nil {
return err
}
switch run.State {
case cyclone.RunCompleted:
fmt.Println("done")
case cyclone.RunFailed:
steps, _ := client.ListSteps(ctx, runID)
for _, s := range steps {
if s.State == cyclone.StepExhausted {
fmt.Printf("failed: %s\n", s.Error)
}
}
case cyclone.RunRunning:
fmt.Println("still running, attempt", /* check steps */)
case cyclone.RunCancelled:
fmt.Println("cancelled")
}Read Output After Completion
The Enqueue method on a step returns a *cyclone.ResultRun[O]. The package function cyclone.Enqueue returns a run ID. Result waits for the run. Result returns the output of the step:
go
run, err := FetchStep.Enqueue(ctx, client, FetchInput{URL: "https://example.com"})
if err != nil {
return err
}
out, err := run.Result(ctx) // out is a FetchOutputFor a standalone job, the node ID is the kind of the step. run.Result reads that output. cyclone.OutputNode[FetchOutput](ctx, client, run.ID(), "fetch") reads the same output. See Read Step Output.
Retry/Discard Behavior
The default configuration:
- Cyclone allows 5 attempts with exponential backoff (1s to ~1h with jitter).
- Cyclone retries every error except an error wrapped with
cyclone.Permanent. - After exhaustion, the step moves to
discarded. The run moves tofailed.
Reopen a discarded step with client.Retry(ctx, runID, stepKind) or client.RetryAll(ctx, runID).
See Retries and Permanent Failures.