A dependency-aware work-coordination engine over SQLite.
Model units of work as issues, link them with a typed dependency DAG, and query the result through a composable spec or a named intent. Bee computes ready sets, critical paths, and effort rollups; records every mutation to an append-only event log; and accepts opaque measurements (cost, tokens, duration) to calibrate its own outputs.
Setup
Add Bee.Supervisor to your supervision tree:
children = [
{Bee.Supervisor, db_path: "priv/bee.db", prefix: "bee"}
]A first workflow
{:ok, a} = Bee.create("Implement auth", priority: 2)
{:ok, b} = Bee.create("Add billing")
:ok = Bee.block(b.id, a.id, type: :blocks)
{:ok, ready} = Bee.ready() # => [%{id: 1, ...}] (billing is gated)
{:ok, %{issues: top, withheld: withheld}} =
Bee.query(status: "open", limit: 5) # `withheld` reports what was truncatedTwo surfaces
This module is the in-process facade. Bee also publishes a versioned GenServer
message protocol ({:create, title, opts}, {:ready, opts}, ...) for node-crossing
consumers — a change safe for one surface is proven safe for the other.
The full guide — dependency vocabulary, the allocation tree, measurements × dimensions, the event log, and configuration — lives in the README, which is the HexDocs landing page.
Summary
Functions
Returns {:ok, [%{id, title, status, issue_type, project_id}]}: the parent chain
of id, root first, excluding id itself ([] for a root). {:error, :not_found} for an unknown id. The walk is bounded, so it terminates even over a
corrupted parent cycle.
Resolves an intent — an atom for a built-in (:what_next) or a binary name
registered via register_intent/3 — into a query spec and runs it.
Atomically links an existing Bee issue to a Work Lane as "root" or
"member". attrs has exactly :command_id, :lane_id, :issue_id
(a full Bee issue ID), :role, and :project_id (the expected Bee
project, or nil). A matching command replays its durable receipt.
Conflicts and missing issues return tagged errors without changing a link.
This command never changes issues.project_id.
Atomically backfill positively qualified issue targets and their audit rows.
Adds a dependency: id is gated by blocker_id. type: selects the dependency
type (default :blocks; see the README for the full vocabulary). Returns :ok or
{:error, :cycle} if the edge would create a cycle. Only gating types feed
ready/0 and rollups.
Bee-owned diagnostic target classification; not a mutation permit.
Counts issues matching opts: the list/2 filters plus the query spec's filter
fields (:under, :include_root, :depth, :labels_any, :issue_types,
:priority_min, :priority_max, :has_children, :blocked). An unknown
:under returns {:error, {:not_found, id}}.
Creates an issue. Returns {:ok, issue}.
Returns {:ok, [ids]} — the longest dependency path through the gating DAG,
computed with a memoized topsort rather than exponential recursion. Pass :root
for the blocker-to-goal chain ending at one issue, or scope the global path with
:project_id and/or :status. :types selects dependency types and :depth
bounds rooted traversal (maximum 100).
Fetches a single issue by id. Returns {:ok, issue} or {:error, :not_found}.
Owner cutover status; diagnostic only, never an admission permit.
Acquires a lock on an issue. Options: :locked_by (holder id), :ttl (minutes,
default 30), :force (take the lock from its current holder).
Records an opaque measurement against an issue. attrs carries :measure,
:value, optional :source, and :dims — a map whose keys must match
[a-z][a-z0-9_]* and which must include "kind" ("estimate" or "actual").
Internal admitted lane issue mutation; caller admission is rechecked inside the owner transaction.
Runs a composable query. Returns
{:ok, %{issues: [...], withheld: map, refine: keyword}}.
Returns {:ok, issues} — every open issue with no gating dependency still
outstanding. Honours :blocks, :waits_for, and :conditional_blocks.
Reads a Bee-owned receipt and its current issue/link state on the writer.
:matching proves the recorded link and project still match; :stale
means they have drifted. :not_found is a snapshot, not proof that an
earlier queued or uncertain command can never commit.
Authenticated owner receipt/current-state read; missing receipt is not an abandonment proof.
Stores a named query spec (persisted in the database). Resolve it later with
ask("name", ...).
Registers a numeric measure (e.g. "effort", "tokens") with a unit and an
optional value :domain (:any or :non_negative). Required before measure/3.
Aggregates effort (measure: "effort") over a scope rooted at id.
Walks the dependency graph from id. direction: :blockers (default) follows
gating edges toward what gates id; :dependents follows them toward what id
gates. Returns {:ok, [ids]}.
Updates an issue with a map of attributes. Returns :ok or
{:error, :not_found | :self_parent | :parent_cycle}.
Canonical registry upsert with original-row comparison inside the owner transaction.
Functions
Returns {:ok, [%{id, title, status, issue_type, project_id}]}: the parent chain
of id, root first, excluding id itself ([] for a root). {:error, :not_found} for an unknown id. The walk is bounded, so it terminates even over a
corrupted parent cycle.
Resolves an intent — an atom for a built-in (:what_next) or a binary name
registered via register_intent/3 — into a query spec and runs it.
Returns the same shape as query/2. Intents are pre-composed queries: one engine,
no drift between a shortcut and the spec it stands for.
Atomically links an existing Bee issue to a Work Lane as "root" or
"member". attrs has exactly :command_id, :lane_id, :issue_id
(a full Bee issue ID), :role, and :project_id (the expected Bee
project, or nil). A matching command replays its durable receipt.
Conflicts and missing issues return tagged errors without changing a link.
This command never changes issues.project_id.
Atomically backfill positively qualified issue targets and their audit rows.
Adds a dependency: id is gated by blocker_id. type: selects the dependency
type (default :blocks; see the README for the full vocabulary). Returns :ok or
{:error, :cycle} if the edge would create a cycle. Only gating types feed
ready/0 and rollups.
Bee-owned diagnostic target classification; not a mutation permit.
Counts issues matching opts: the list/2 filters plus the query spec's filter
fields (:under, :include_root, :depth, :labels_any, :issue_types,
:priority_min, :priority_max, :has_children, :blocked). An unknown
:under returns {:error, {:not_found, id}}.
Runs on a pooled reader in the caller's process, not through the writer.
Creates an issue. Returns {:ok, issue}.
Supported options: :description, :priority (integer), :issue_type (or :type,
default "task"), :labels / :label, :parent (an issue id), :project_id,
:actor (the creator), and :metadata (a map; reserved prefixes are bee: and _).
Returns {:ok, [ids]} — the longest dependency path through the gating DAG,
computed with a memoized topsort rather than exponential recursion. Pass :root
for the blocker-to-goal chain ending at one issue, or scope the global path with
:project_id and/or :status. :types selects dependency types and :depth
bounds rooted traversal (maximum 100).
Fetches a single issue by id. Returns {:ok, issue} or {:error, :not_found}.
By default relations are not loaded (comments: :not_loaded). Pass include:
to load them:
{:ok, issue} = Bee.get(1, include: [:comments])
Owner cutover status; diagnostic only, never an admission permit.
Acquires a lock on an issue. Options: :locked_by (holder id), :ttl (minutes,
default 30), :force (take the lock from its current holder).
Returns {:ok, lock} or {:error, :already_locked | :not_found}. Expired locks
are released by a background sweeper.
Records an opaque measurement against an issue. attrs carries :measure,
:value, optional :source, and :dims — a map whose keys must match
[a-z][a-z0-9_]* and which must include "kind" ("estimate" or "actual").
Internal admitted lane issue mutation; caller admission is rechecked inside the owner transaction.
Runs a composable query. Returns
{:ok, %{issues: [...], withheld: map, refine: keyword}}.
withheld reports omissions (a truncated limit, omitted relations) and refine
suggests how to ask for what was held back. Filter by plain-text :text, :status,
one :project_id or any of :project_ids, :ready, :blocked, :assigned_to,
:labels (all of) or :labels_any (any of), :issue_types, :priority_min /
:priority_max, and :has_children; sort with :order_by; page with :limit and
either :offset or an :after cursor; load relations with :include; and shape
each result with :detail (:minimal | :compact | :standard | :full).
The result also carries total (the match count without limit/after) and
next, a keyset cursor for the following page (or nil). Pass it back as :after
with the same spec: unlike an offset it stays stable while issues are created or
closed between pages.
Parent trees
:under— only strict descendants of that issue over parent edges (include_root: trueadds the issue itself at depth 0). An unknown id returns{:error, {:not_found, id}}.:depth(0..10) — the deepest level returned; each issue then carriesdepth. With:under, children are depth 1. Without it, depth counts from the nearest ancestor that passes the:statusfilter, so an open issue under a closed parent is depth 0 in an open-only view.fold: true(requires:depth) — each issue carriesrollup, counts over its whole real subtree whatever the other filters say.path: true— each issue carriespath, its ancestors from the top down to its parent, closed ones included.keep_ancestors: true— the ancestors of each match come back too, markedcontext: trueand listed first; they do not count towardlimitortotal.
Runs on a pooled reader in the caller's process, not through the writer.
Returns {:ok, issues} — every open issue with no gating dependency still
outstanding. Honours :blocks, :waits_for, and :conditional_blocks.
Reads a Bee-owned receipt and its current issue/link state on the writer.
:matching proves the recorded link and project still match; :stale
means they have drifted. :not_found is a snapshot, not proof that an
earlier queued or uncertain command can never commit.
Authenticated owner receipt/current-state read; missing receipt is not an abandonment proof.
Stores a named query spec (persisted in the database). Resolve it later with
ask("name", ...).
Registers a numeric measure (e.g. "effort", "tokens") with a unit and an
optional value :domain (:any or :non_negative). Required before measure/3.
Aggregates effort (measure: "effort") over a scope rooted at id.
Options: :scope (:tree | :closure | :critical_path, default :tree), |
:kind (:estimate | :actual, default :actual), :meaning (:spent |
:to_complete, default :spent). If any node lacks a measurement, total is
nil, a partial_total is given, and refine lists the missing ids — bee never
silently invents a sum.
Walks the dependency graph from id. direction: :blockers (default) follows
gating edges toward what gates id; :dependents follows them toward what id
gates. Returns {:ok, [ids]}.
Updates an issue with a map of attributes. Returns :ok or
{:error, :not_found | :self_parent | :parent_cycle}.
A :measure key may be folded into the update to record a measurement atomically
with the status change that earned it.
Canonical registry upsert with original-row comparison inside the owner transaction.