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 truncated

Two 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

agent_load(agent_id, server \\ Bee.Repo)

ancestors(id, server \\ Bee.Repo)

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.

ask(intent, opts \\ [], server \\ Bee.Repo)

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.

assign(issue_id, agent_id, server \\ Bee.Repo)

associate_lane_issue(attrs, server \\ Bee.Repo)

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.

backfill_projects(rows, run_id, now, server \\ Bee.Repo)

Atomically backfill positively qualified issue targets and their audit rows.

block(id, blocker_id, server \\ Bee.Repo)

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.

block(id, blocker_id, opts, server)

bottlenecks(server \\ Bee.Repo)

candidates(id, server \\ Bee.Repo)

classify_lane_write(request, server \\ Bee.Repo)

Bee-owned diagnostic target classification; not a mutation permit.

comment(id, text, opts \\ [], server \\ Bee.Repo)

count(opts \\ [], server \\ Bee.Repo)

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.

create(title, opts \\ [], server \\ Bee.Repo)

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 _).

critical_path()

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).

critical_path(opts)

critical_path(opts, server)

get(id)

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])

get(id, opts)

get(id, opts, server)

get_comments(id, server \\ Bee.Repo)

import_jsonl(path, server \\ Bee.Repo)

join_project(agent_id, project_id, server \\ Bee.Repo)

lane_write_fence_status(server \\ Bee.Repo)

Owner cutover status; diagnostic only, never an admission permit.

list(opts \\ [], server \\ Bee.Repo)

list_agents(server \\ Bee.Repo)

list_intents(server \\ Bee.Repo)

list_measures(server \\ Bee.Repo)

list_projects(server \\ Bee.Repo)

lock(id, opts \\ [], server \\ Bee.Repo)

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.

measure(id, attrs, server \\ Bee.Repo)

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").

mutate_lane_issue(attrs, admission_check, server \\ Bee.Repo)

Internal admitted lane issue mutation; caller admission is rechecked inside the owner transaction.

query(spec, server \\ Bee.Repo)

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: true adds the issue itself at depth 0). An unknown id returns {:error, {:not_found, id}}.
  • :depth (0..10) — the deepest level returned; each issue then carries depth. With :under, children are depth 1. Without it, depth counts from the nearest ancestor that passes the :status filter, so an open issue under a closed parent is depth 0 in an open-only view.
  • fold: true (requires :depth) — each issue carries rollup, counts over its whole real subtree whatever the other filters say.
  • path: true — each issue carries path, its ancestors from the top down to its parent, closed ones included.
  • keep_ancestors: true — the ancestors of each match come back too, marked context: true and listed first; they do not count toward limit or total.

Runs on a pooled reader in the caller's process, not through the writer.

ready(opts \\ [], server \\ Bee.Repo)

Returns {:ok, issues} — every open issue with no gating dependency still outstanding. Honours :blocks, :waits_for, and :conditional_blocks.

reconcile_lane_issue(command_id, server \\ Bee.Repo)

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.

reconcile_lane_mutation(command, principal, lane, admission_check, server \\ Bee.Repo)

Authenticated owner receipt/current-state read; missing receipt is not an abandonment proof.

register_agent(id, attrs \\ %{}, server \\ Bee.Repo)

register_intent(name, spec, server \\ Bee.Repo)

Stores a named query spec (persisted in the database). Resolve it later with ask("name", ...).

register_measure(name, unit)

Registers a numeric measure (e.g. "effort", "tokens") with a unit and an optional value :domain (:any or :non_negative). Required before measure/3.

register_measure(name, unit, opts)

register_measure(name, unit, opts, server)

register_project(id, attrs \\ %{}, server \\ Bee.Repo)

remove_intent(name, server \\ Bee.Repo)

rollup(id, opts \\ [], server \\ Bee.Repo)

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.

traverse(id, opts \\ [], server \\ Bee.Repo)

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]}.

tree_page(opts \\ [], server \\ Bee.Repo)

unblock(id, blocker_id, server \\ Bee.Repo)

unblock(id, blocker_id, opts, server)

unlock(id, server \\ Bee.Repo)

update(id, attrs, server \\ Bee.Repo)

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.

upsert_project_registry(id, attrs, expected, server \\ Bee.Repo)

Canonical registry upsert with original-row comparison inside the owner transaction.

who_blocks_whom(server \\ Bee.Repo)