Skivi is a PostgreSQL-backed job queue library for Clojure, in the spirit of Graphile Worker. It runs inside your existing database, creating its own schema there rather than requiring a separate service. This means you can enqueue jobs within the same transaction that triggers them, with no extra infrastructure to operate.
{:deps {dev.skivi/skivi {:mvn/version "RELEASE"}}}
A system is created from configuration, a task registry, and an optional list of cron entries, then started:
(def system
(-> (skivi/create-system config tasks)
(skivi/start!)))
;; Enqueue from anywhere in your application
(skivi/add-job system "send-email" {:to "user@example.com" :subject "Hello"})
start! runs pending database migrations automatically (this can be disabled
with {:migrate? false}), then starts the worker pool and any cron scheduler.
stop! drains in-flight jobs gracefully before closing connections.
The worker pool polls the database for available jobs, claiming them with advisory locks to prevent double-processing. Each job is dispatched to its registered handler function. A normal return marks the job complete; any uncaught exception triggers a retry with exponential backoff. After exhausting its allowed attempts, a job is marked exhausted and recorded in history.
Task handlers are plain Clojure functions keyed by string identifier. There are two ways to register them.
Programmatic registry – pass a map to create-system:
(def tasks
{"send-email" (fn [{:keys [job]}] (send! (:payload job)))
"resize-image" (fn [{:keys [job]}] (resize! (:payload job)))})
(skivi/create-system config tasks)
File-based loading – set :task-directory in the worker config and drop
.clj files there. Each file must return a registry map as its last expression:
;; tasks/email.clj
{"send-email" (fn [{:keys [job]}]
(send! (:payload job)))}
;; skivi-config.edn
{:worker {:task-directory "tasks"
:file-extensions [".clj"] ; optional, defaults to [".clj"]
...}}
Skivi loads all matching files at startup, sorted alphabetically, and merges their registries. If you also pass a programmatic registry, it wins on any identifier that appears in both. A job whose task identifier has no registered handler is immediately exhausted rather than retried – a missing handler is a configuration error, not a transient failure.
Skivi creates all its tables, functions, and triggers in a dedicated PostgreSQL
schema within your existing application database. The default schema name is
skivi, though it is configurable if you need multiple instances in the same
database or prefer a different name:
;; skivi-config.edn
{:database {:connection-string "postgresql://localhost:5432/my_app"
:schema-name "jobs"}}
Cron entries are defined as data and passed to create-system:
(def crontabs
[{:identifier "daily-report"
:schedule "0 2 * * *"
:spec {:queue-name "maintenance" :max-attempts 3}}
{:identifier "hourly-cleanup"
:schedule "0 * * * *"}])
(skivi/create-system config tasks crontabs)
Each cron entry uses unsafe_dedupe by default: if the previous run’s job is
still queued or in flight, the new enqueue is silently skipped. Last-execution
state is persisted to the database, so the scheduler survives restarts without
double-firing.
The event emitter lets you attach handlers to specific event types or to all events:
(monitoring/on (:emitter system) :all
(fn [{:keys [type data]}]
(log/info "event" (assoc data :event/type type))))
(monitoring/on (:emitter system) :job/exhausted
(fn [{:keys [data]}]
(alert! "Job exhausted after all attempts" data)))
Standard event types include :job/completed, :job/failed, :job/exhausted,
:job/partial-success, :pool/start, :pool/stop, and :cron/fired. The
emitter also maintains an in-memory ring buffer of recent events, useful in
tests via collecting-emitter.
Task payloads are validated before they reach the database. You can declare
malli schemas or clojure.spec specs per task identifier in configuration,
and validation errors surface at enqueue time rather than during execution.
Skivi is dual-licensed. Open-source use is covered by the GNU Affero General Public License v3.0 or later. Commercial licenses are available if the AGPL is not suitable for your use case.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |