Programmatic interface for the filigree issue tracker. All public classes and functions are importable from the filigree package or its submodules.
See also: CLI Reference | MCP Server | Architecture | Workflows
Surface scope. This page documents the in-process Python API (
FiligreeDBand helpers). It returns native Python objects (Issue, tuples,None) and raises native exceptions. The unified 2.0 response envelopes (BatchResponse,ListResponse,ErrorResponsewith closedErrorCode) live one layer up, on the MCP and CLI surfaces — see MCP Server, CLI Reference, and Agent Integration. The mapping fromValueError/KeyErrorhere toErrorCodeon the wire is infiligree.types.api.classify_value_error.
from filigree import FiligreeDB
with FiligreeDB.from_project() as db:
issue = db.create_issue("Fix login bug", type="bug", priority=1)
print(issue.id, issue.status)from filigree import FiligreeDBThe central class for all issue tracker operations. Wraps a SQLite database with WAL mode, providing direct read/write access with no daemon or sync layer.
FiligreeDB(
db_path: str | Path,
*,
prefix: str = "filigree",
enabled_packs: list[str] | None = None,
template_registry: TemplateRegistry | None = None,
) -> None| Parameter | Type | Default | Description |
|---|---|---|---|
db_path |
str | Path |
(required) | Path to the SQLite database file |
prefix |
str |
"filigree" |
Prefix for generated issue IDs (e.g. "myproject" yields myproject-a3f) |
enabled_packs |
list[str] | None |
None |
Workflow packs to enable. None reads from config; defaults to ["core", "planning", "release"] |
template_registry |
TemplateRegistry | None |
None |
Inject a pre-configured registry (useful for testing). None creates one lazily |
@classmethod
FiligreeDB.from_project(project_path: Path | None = None) -> FiligreeDBDiscovers the .filigree/ directory by walking up from project_path (or the current working directory), reads config.json, creates the database connection, and calls initialize(). Returns a ready-to-use instance.
Raises FileNotFoundError if no .filigree/ directory is found.
FiligreeDB supports the context manager protocol. The connection is closed on exit:
with FiligreeDB.from_project() as db:
db.create_issue("My task")
# db.close() called automatically| Property | Type | Description |
|---|---|---|
conn |
sqlite3.Connection |
Lazy-opened SQLite connection with WAL mode, foreign keys, and 5s busy timeout |
templates |
TemplateRegistry |
Lazy-loaded template registry. Created on first access from .filigree/ config |
def initialize(self) -> NoneCreates tables, runs pending schema migrations, and seeds built-in templates. Called automatically by from_project(). Safe to call multiple times (idempotent).
def close(self) -> NoneCloses the underlying SQLite connection. Safe to call multiple times.
def reload_templates(self) -> NoneClears the cached TemplateRegistry so it reloads from disk on next access. Use after editing .filigree/templates/ or .filigree/packs/ files at runtime.
def get_schema_version(self) -> intReturns the current database schema version (from SQLite PRAGMA user_version).
The Python core keeps the stored dataclass names: Issue.id and
Issue.parent_id. MCP and CLI JSON expose the agent-facing 2.0 vocabulary:
issue_id for issue primary keys and parent_issue_id for hierarchy links.
Full public issue payloads also include parent_id as a compatibility alias
with the same value; new callers should prefer parent_issue_id, and
parent_id may be removed from public wire payloads in a future major.
Dependency APIs describe the edge direction explicitly. In MCP, from_issue_id
is the issue that is blocked, and to_issue_id is the issue that blocks it.
The Python core uses the same relationship as issue_id depends on
depends_on_id.
def create_issue(
self,
title: str,
*,
type: str = "task",
priority: int = 2,
parent_id: str | None = None,
assignee: str = "",
description: str = "",
notes: str = "",
fields: dict[str, Any] | None = None,
labels: list[str] | None = None,
deps: list[str] | None = None,
actor: str = "",
) -> IssueCreates a new issue. The initial status is determined by the type's template (typically "open"). Generates a unique ID using the configured prefix.
| Parameter | Type | Default | Description |
|---|---|---|---|
title |
str |
(required) | Issue title. Cannot be empty |
type |
str |
"task" |
Issue type. Must be a registered type |
priority |
int |
2 |
Priority 0-4 (0=critical, 4=backlog) |
parent_id |
str | None |
None |
Parent issue ID for hierarchy |
assignee |
str |
"" |
Assignee name |
description |
str |
"" |
Detailed description |
notes |
str |
"" |
Additional notes |
fields |
dict[str, Any] | None |
None |
Custom fields defined by the type's template |
labels |
list[str] | None |
None |
Labels to attach |
deps |
list[str] | None |
None |
Issue IDs this issue depends on |
actor |
str |
"" |
Identity for the audit trail |
Returns: The created Issue.
Raises: ValueError if the title is empty, priority is out of range, type is unknown, or parent_id is invalid.
def get_issue(self, issue_id: str) -> IssueRetrieves a single issue with all computed fields (labels, dependencies, children, readiness).
Raises: KeyError if the issue does not exist.
def update_issue(
self,
issue_id: str,
*,
title: str | None = None,
status: str | None = None,
priority: int | None = None,
assignee: str | None = None,
description: str | None = None,
notes: str | None = None,
parent_id: str | None = None,
fields: dict[str, Any] | None = None,
actor: str = "",
expected_assignee: str | None = None,
) -> IssueUpdates one or more fields on an existing issue. Only provided (non-None) fields are changed. Status transitions are validated against the type's workflow template. Fields are merged into the existing fields dict (not replaced).
Pass parent_id="" to clear the parent. Self-parenting and circular parent chains are rejected.
Soft workflow enforcement does not block the update. When a status change skips
recommended fields, the returned Issue.to_dict()["data_warnings"] includes
the advisory, and the same message is recorded once as a transition_warning
event.
When actor is present and the issue is held, claim-aware write safety defaults
the expected holder to actor. Pass expected_assignee for coordinator
compare-and-swap writes against another observed holder. Mismatches raise
ValueError naming both holders and surface as CONFLICT on API/MCP/CLI
boundaries.
Returns: The updated Issue.
Raises:
KeyErrorif the issue does not exist.ValueErrorif the status transition is not allowed, required fields are missing (hard enforcement), priority is out of range, or parent_id would create a cycle.
def close_issue(
self,
issue_id: str,
*,
reason: str = "",
actor: str = "",
status: str | None = None,
fields: dict[str, Any] | None = None,
expected_assignee: str | None = None,
force: bool = False,
) -> IssueCloses an issue by moving it to a done-category state. Close uses the same
forward transition validation as update_issue() by default. With
force=True, close validates against the template's reverse_transitions
escape lane instead, emits transition_forced, and does not inherit normal
target-state required_at gates. Sets closed_at automatically.
| Parameter | Type | Default | Description |
|---|---|---|---|
issue_id |
str |
(required) | Issue to close |
reason |
str |
"" |
Stored in fields.close_reason |
actor |
str |
"" |
Identity for the audit trail |
status |
str | None |
None |
Specific done-category state. None uses the first done state from the template |
fields |
dict[str, Any] | None |
None |
Additional fields to merge while closing |
force |
bool |
False |
Use the declared reverse/escape edge for cleanup closes |
The close reason is stored in fields.close_reason; a reason-only close also
records the text on the status-change event comment so history readers can
display it without reconstructing field deltas.
Raises: InvalidTransitionError (a ValueError subclass) with
valid_transitions when the current status cannot reach the close target;
ValueError if the issue is already closed or the specified status is not a
done-category state.
def reopen_issue(self, issue_id: str, *, actor: str = "") -> IssueReopens a closed issue, returning it to the last non-done status before closure.
Clears closed_at and stale close-only fields such as close_reason.
Raises: ValueError if the issue is not in a done-category state.
def list_issues(
self,
*,
status: str | None = None,
type: str | None = None,
priority: int | None = None,
parent_id: str | None = None,
assignee: str | None = None,
label: str | None = None,
limit: int = 100,
offset: int = 0,
) -> list[Issue]Lists issues with optional filters. Results are sorted by priority then creation time. All filters are ANDed together.
The status parameter supports both literal state names (e.g. "triaged") and category aliases: passing "open", "in_progress"/"wip", or "closed"/"done" expands to all states in that category across all registered types.
def search_issues(
self,
query: str,
*,
limit: int = 100,
offset: int = 0,
) -> list[Issue]Full-text search over issue titles and descriptions. Uses SQLite FTS5 with prefix matching. Falls back to LIKE if FTS is unavailable.
def claim_issue(
self,
issue_id: str,
*,
assignee: str,
actor: str = "",
) -> IssueAtomically claims an issue by setting its assignee. Uses optimistic locking -- the issue must be in an open-category state and either unassigned or already assigned to the same assignee. Does not change status.
Raises:
KeyErrorif the issue does not exist.ValueErrorif the issue is already assigned to someone else or not in an open-category state.
def claim_next(
self,
assignee: str,
*,
type_filter: str | None = None,
priority_min: int | None = None,
priority_max: int | None = None,
actor: str = "",
) -> Issue | NoneClaims the highest-priority ready issue matching the filters. Iterates ready issues and attempts claim_issue() on each, handling race conditions with retry.
Returns: The claimed Issue, or None if no matching ready issues exist.
def release_claim(
self,
issue_id: str,
*,
actor: str = "",
if_held: bool = False,
expected_assignee: str | None = None,
reason: str = "",
) -> IssueReleases a claimed issue by clearing its assignee. Does not change status.
By default this is strict and raises when the issue is already unassigned. With
if_held=True, unassigned issues are returned unchanged, but assigned issues are
only released when held by expected_assignee or, if omitted, actor;
held-by-other mismatches raise ClaimConflictError.
Raises: ValueError if strict mode sees no assignee; ClaimConflictError
if if_held=True would clear a claim held by someone other than the expected
holder.
def heartbeat_work(
self,
issue_id: str,
*,
actor: str = "",
expected_assignee: str | None = None,
lease_hours: int = 48,
) -> IssueRefreshes liveness metadata for a claimed, non-done issue. The current assignee
must match expected_assignee when provided, otherwise actor is treated as the
expected holder when non-empty. Updates last_heartbeat_at and
claim_expires_at.
def get_stale_claims(
self,
*,
stale_after_hours: int = 48,
expires_within_hours: int | None = None,
) -> list[Issue]Returns assigned, non-done issues with expired claim_expires_at values. Legacy
assigned rows without explicit lease metadata fall back to last_heartbeat_at,
claimed_at, or updated_at and are stale when older than the threshold. Pass
expires_within_hours to include active explicit leases expiring soon enough
for proactive heartbeating.
def reclaim_issue(
self,
issue_id: str,
*,
assignee: str,
expected_assignee: str,
reason: str,
actor: str = "",
lease_hours: int = 48,
) -> IssueTransfers a claim to assignee only when the current holder still matches
expected_assignee. Records reason on the reclaim event and starts a fresh
lease for the new holder.
def start_work(
self,
issue_id: str,
*,
assignee: str,
target_status: str | None = None,
actor: str = "",
) -> IssueComposed 2.0 operation: atomically claim an issue and transition it to a working status in one call. Performs claim_issue followed by update_issue(status=target_status) with a compensating-action rollback — if the transition fails, the claim is released. Rollback only fires when this call acquired the claim, so a pre-existing same-assignee claim is preserved.
target_status defaults to the unique wip-category status reachable from the issue's current status. If the current status has multiple reachable wip-category targets, raises AmbiguousTransitionError (caller must specify target_status explicitly); if zero, raises InvalidTransitionError.
This is the recommended path for picking up work in 2.0 — claim_issue remains for the niche "reserve without transitioning" case.
Raises:
KeyErrorif the issue does not exist.ValueError(CONFLICT) if the issue is already assigned to someone else.AmbiguousTransitionError/InvalidTransitionErrorfrom working-status resolution.
def start_next_work(
self,
*,
assignee: str,
type_filter: str | None = None,
priority_min: int | None = None,
priority_max: int | None = None,
target_status: str | None = None,
actor: str = "",
) -> Issue | NoneComposed 2.0 operation: claim the highest-priority ready issue matching the filters and atomically transition it to a working status. Same rollback contract as start_work. Tie-break ordering inherits from claim_next (priority asc, created_at asc, issue_id asc).
Returns: The claimed and transitioned Issue, or None if no matching ready issue exists.
def batch_close(
self,
issue_ids: list[str],
*,
reason: str = "",
actor: str = "",
expected_assignee: str | None = None,
force: bool = False,
) -> tuple[list[Issue], list[dict[str, str]]]Closes multiple issues with per-item error handling. Each issue is closed via close_issue().
Returns: A 2-tuple of (closed_issues, errors) where each error is {"id": str, "error": str}.
def batch_update(
self,
issue_ids: list[str],
*,
status: str | None = None,
priority: int | None = None,
assignee: str | None = None,
fields: dict[str, Any] | None = None,
actor: str = "",
expected_assignee: str | None = None,
) -> tuple[list[Issue], list[dict[str, str]]]Applies the same changes to multiple issues. Errors on individual issues do not abort the batch.
Returns: A 2-tuple of (updated_issues, errors) where each error is {"id": str, "error": str}.
def register_file(
self,
path: str,
*,
language: str = "",
file_type: str = "",
metadata: dict[str, Any] | None = None,
actor: str = "",
) -> FileRecordUpserts a file record by project-relative path and returns the resulting
FileRecord. File records expose created_by and updated_by; metadata
update events in file timelines include the actor.
def list_files(
self,
*,
limit: int = 100,
offset: int = 0,
language: str | None = None,
path_prefix: str | None = None,
sort: str = "updated_at",
) -> list[FileRecord]Lists tracked files with optional filtering and sorting.
def get_file(self, file_id: str) -> FileRecordReturns one file record by ID.
def delete_file_record(self, file_id: str, *, force: bool = False, actor: str = "") -> dict[str, Any]Deletes a file record plus file-domain cleanup rows. Without force, refuses records that still have issue associations or non-terminal findings; with force, cascades associations and findings and unlinks observations from the deleted file.
def get_file_timeline(
self,
file_id: str,
*,
limit: int = 50,
offset: int = 0,
event_type: str | None = None,
include_issue_events: bool = False,
) -> dict[str, Any]Returns merged finding/association/metadata events for a file with pagination metadata.
Pass include_issue_events=True to merge events from associated issues; event_type="issue_event" filters to those issue-side events.
def get_issue_files(self, issue_id: str) -> list[dict[str, Any]]Lists files associated with an issue.
def add_file_association(
self,
file_id: str,
issue_id: str,
assoc_type: str,
*,
actor: str = "",
) -> NoneCreates an idempotent file<->issue association. Association records and association timeline events include the actor.
def add_dependency(
self,
issue_id: str,
depends_on_id: str,
*,
dep_type: str = "blocks",
actor: str = "",
) -> boolAdds a dependency: issue_id depends on (is blocked by) depends_on_id. Validates both issues exist and rejects self-dependencies and cycles.
Returns: True if the dependency was created, False if it already existed.
Raises:
KeyErrorif either issue does not exist.ValueErrorfor self-dependencies or if the dependency would create a cycle.
def remove_dependency(
self,
issue_id: str,
depends_on_id: str,
*,
actor: str = "",
) -> boolRemoves a dependency between two issues.
Returns: True if removed, False if the dependency did not exist.
def get_all_dependencies(self) -> list[dict[str, str]]Returns all dependencies as a list of {"from": str, "to": str, "type": str} dicts, where "from" is the blocked issue and "to" is the blocker.
def get_ready(self) -> list[Issue]Returns issues in open-category states with no unresolved blockers, sorted by priority then creation time.
def get_blocked(self) -> list[Issue]Returns issues in open-category states that have at least one non-done blocker.
def get_critical_path(self) -> list[dict[str, Any]]Computes the longest dependency chain among non-done issues using topological-order dynamic programming.
Returns: The chain as a list of {"id": str, "title": str, "priority": int, "type": str} dicts, ordered from root blocker to final blocked issue. Empty list if no chains exist.
def get_plan(self, milestone_id: str) -> dict[str, Any]Retrieves the milestone/phase/step hierarchy with progress statistics.
Returns:
{
"milestone": dict, # Issue.to_dict()
"phases": [
{
"phase": dict, # Issue.to_dict()
"steps": [dict, ...],
"total": int,
"completed": int,
"ready": int,
},
],
"total_steps": int,
"completed_steps": int,
}def create_plan(
self,
milestone: dict[str, Any],
phases: list[dict[str, Any]],
*,
actor: str = "",
) -> dict[str, Any]Creates a full milestone, phase, and step hierarchy in one transaction.
| Parameter | Type | Description |
|---|---|---|
milestone |
dict |
{"title": str, "priority?": int, "description?": str, "fields?": dict} |
phases |
list[dict] |
[{"title": str, "priority?": int, "description?": str, "steps": [{"title": str, "deps?": [int | str]}]}] |
actor |
str |
Identity for the audit trail |
Step dependencies use integer indices (0-based within the same phase) or cross-phase references as "phase_idx.step_idx" strings.
Returns: The full plan tree (same format as get_plan()).
Raises: ValueError if any title is empty.
def add_comment(
self,
issue_id: str,
text: str,
*,
author: str = "",
expected_assignee: str | None = None,
) -> intAdds a comment to an issue.
Returns: The comment's integer ID.
Raises: ValueError if text is empty.
Use get_comment(comment_id) or get_comments(issue_id) to retrieve the
structured row (id, author, text, created_at). MCP and CLI add-comment
write responses echo that structured comment as comment with the primary key
renamed to comment_id.
def get_comment(self, comment_id: int) -> CommentRecordReturns one comment row by ID.
def get_comments(self, issue_id: str) -> list[CommentRecord]Returns all comments on an issue, ordered chronologically. Each record contains id, author, text, and created_at.
def add_label(
self,
issue_id: str,
label: str,
*,
actor: str = "",
expected_assignee: str | None = None,
) -> tuple[bool, str, list[str]]Adds a label to an issue. Returns: (added, canonical_label, replaced_labels).
def remove_label(
self,
issue_id: str,
label: str,
*,
actor: str = "",
expected_assignee: str | None = None,
) -> tuple[bool, str]Removes a label from an issue. Returns: (removed, canonical_label).
def get_stats(self) -> dict[str, Any]Returns project statistics:
{
"by_status": {"open": 5, "in_progress": 2, ...},
"by_category": {"open": 5, "wip": 2, "done": 10},
"status_name_counts": {"open": 5, "in_progress": 2, ...},
"status_category_counts": {"open": 5, "wip": 2, "done": 10},
"by_type": {"task": 8, "bug": 4, ...},
"ready_count": int,
"blocked_count": int,
"total_dependencies": int,
}by_status holds counts keyed by literal workflow status name; by_category
holds template-aware category counts (open/wip/done). status_name_counts
and status_category_counts are deprecated exact duplicates of by_status
and by_category respectively (filigree-17694d2db8), retained as compatibility
aliases per ADR-009 §7 and scheduled for removal in the next major. Read
by_status / by_category.
def get_recent_events(self, limit: int = 20) -> list[dict[str, Any]]Returns the most recent events across all issues, newest first. Each dict includes all event fields plus issue_title.
def get_events_since(self, since: str, *, limit: int = 100) -> list[dict[str, Any]]Returns events after the given ISO timestamp, ordered chronologically (oldest first). Useful for session resumption and polling.
def get_issue_events(self, issue_id: str, *, limit: int = 50) -> list[dict[str, Any]]Returns events for a specific issue, newest first.
Raises: KeyError if the issue does not exist.
def undo_last(self, issue_id: str, *, actor: str = "") -> dict[str, Any]Undoes the most recent reversible event for an issue. Reversible events: status_changed, title_changed, priority_changed, assignee_changed, claimed, dependency_added, dependency_removed, description_changed, notes_changed.
Returns:
# Success:
{"undone": True, "event_type": str, "event_id": int, "issue": dict}
# Nothing to undo:
{"undone": False, "reason": str}def get_template(self, issue_type: str) -> dict[str, Any] | NoneReturns the canonical workflow definition for a type as a dict with type,
display_name, description, pack, states, initial_state,
transitions, reverse_transitions, and fields_schema. Returns None if
the type is not registered.
def list_templates(self) -> list[dict[str, Any]]Lists all registered templates (respects enabled_packs), sorted by type name. Each dict contains type, display_name, description, and fields_schema.
def get_valid_transitions(self, issue_id: str) -> list[TransitionOption]Returns valid next states for an issue with readiness indicators. Each TransitionOption shows which fields are needed before the transition can proceed. See TransitionOption below.
def validate_issue(self, issue_id: str) -> ValidationResultValidates an issue against its type template. Checks fields required at the current state and fields needed for upcoming transitions. See ValidationResult below.
def export_jsonl(self, output_path: str | Path) -> intExports all issues, dependencies, labels, comments, and events to a JSONL file. Each line is a JSON object with a _type field ("issue", "dependency", "label", "comment", "event").
Returns: Total number of records written.
def import_jsonl(self, input_path: str | Path, *, merge: bool = False) -> intImports records from a JSONL file.
| Parameter | Type | Default | Description |
|---|---|---|---|
input_path |
str | Path |
(required) | Path to the JSONL file |
merge |
bool |
False |
If True, skips existing records. If False, raises on conflict |
Returns: Number of records imported.
def archive_closed(self, *, days_old: int = 30, actor: str = "", label: str | None = None) -> list[str]Archives issues that have been closed for more than days_old days by setting their status to "archived".
When label is provided, only closed issues currently carrying that label are archived.
Returns: List of archived issue IDs.
def compact_events(self, *, keep_recent: int = 50, actor: str = "") -> intRemoves old events for archived issues, keeping only the keep_recent most recent events per issue.
Returns: Number of events deleted.
from filigree import IssueA mutable dataclass representing an issue with both stored and computed fields.
| Field | Type | Default | Description |
|---|---|---|---|
id |
str |
(required) | Unique identifier (e.g. "myproject-a3f") |
title |
str |
(required) | Issue title |
status |
str |
"open" |
Current workflow state |
priority |
int |
2 |
Priority level 0-4 (0=critical) |
type |
str |
"task" |
Issue type (e.g. "task", "bug", "feature") |
parent_id |
str | None |
None |
Parent issue ID for hierarchy |
assignee |
str |
"" |
Assigned agent or user |
created_at |
str |
"" |
ISO 8601 creation timestamp |
updated_at |
str |
"" |
ISO 8601 last-update timestamp |
closed_at |
str | None |
None |
ISO 8601 close timestamp, or None if open |
description |
str |
"" |
Detailed description |
notes |
str |
"" |
Additional notes |
fields |
dict[str, Any] |
{} |
Custom fields defined by the type's template |
Computed fields (populated by FiligreeDB when retrieving issues):
| Field | Type | Default | Description |
|---|---|---|---|
labels |
list[str] |
[] |
Attached labels |
blocks |
list[str] |
[] |
Issue IDs that this issue blocks |
blocked_by |
list[str] |
[] |
Issue IDs blocking this issue (only non-done blockers) |
is_ready |
bool |
False |
True if in open-category state with no unresolved blockers |
children |
list[str] |
[] |
Child issue IDs |
status_category |
str |
"open" |
Resolved category: "open", "wip", or "done" |
def to_dict(self) -> dict[str, Any]Serializes the issue to a plain dict suitable for JSON output.
from filigree.templates import TemplateRegistryLoads, caches, and queries workflow templates and packs. Templates are loaded once per instance and cached for the entire lifetime. Typically accessed via FiligreeDB.templates rather than instantiated directly.
TemplateRegistry() -> NoneCreates an empty registry. Call load() to populate it.
def load(
self,
filigree_dir: Path,
*,
enabled_packs: list[str] | None = None,
) -> NoneLoads templates from three layers (later layers override earlier ones):
- Built-in packs from
filigree.templates_data.BUILT_IN_PACKS - Installed packs from
.filigree/packs/*.json - Project-local overrides from
.filigree/templates/*.json
Idempotent: a second call is a no-op.
| Method | Signature | Description |
|---|---|---|
get_type |
(type_name: str) -> TypeTemplate | None |
Get a type template by name |
get_pack |
(pack_name: str) -> WorkflowPack | None |
Get a workflow pack by name |
list_types |
() -> list[TypeTemplate] |
All types from enabled packs |
list_packs |
() -> list[WorkflowPack] |
All enabled packs |
get_initial_state |
(type_name: str) -> str |
Initial state for a type. Falls back to "open" |
get_category |
(type_name: str, state: str) -> StateCategory | None |
Map (type, state) to category via O(1) cache |
get_valid_states |
(type_name: str) -> list[str] | None |
Valid state names for a type. None if unknown |
get_first_state_of_category |
(type_name: str, category: StateCategory) -> str | None |
First state of a given category |
def validate_transition(
self,
type_name: str,
from_state: str,
to_state: str,
fields: dict[str, Any],
) -> TransitionResultValidates a state transition. Unknown types allow all transitions (permissive fallback).
Returns: A TransitionResult indicating whether the transition is allowed.
def get_valid_transitions(
self,
type_name: str,
current_state: str,
fields: dict[str, Any],
) -> list[TransitionOption]Returns all valid transitions from the current state with readiness info.
def validate_fields_for_state(
self,
type_name: str,
state: str,
fields: dict[str, Any],
) -> list[str]Returns field names that are required at the given state but not yet populated.
@staticmethod
TemplateRegistry.parse_type_template(raw: dict[str, Any]) -> TypeTemplateParses a type template from a JSON-compatible dict. Enforces size limits (max 50 states, 200 total forward + reverse transitions, 50 fields).
Raises: ValueError for invalid data, KeyError for missing required keys.
@staticmethod
TemplateRegistry.validate_type_template(tpl: TypeTemplate) -> list[str]Validates a TypeTemplate for internal consistency (state references, field references).
Returns: List of error messages. Empty list means valid.
All template data types are frozen (immutable) dataclasses defined in filigree.templates.
The runtime semantics contract for initial states, type-aware status
categories, hard/soft enforcement, warnings, close/reopen target selection, and
claim handoff is documented in Workflow Templates.
from filigree.templates import (
StateDefinition,
TransitionDefinition,
FieldSchema,
TypeTemplate,
WorkflowPack,
TransitionResult,
TransitionOption,
ValidationResult,
)A named state within a type's workflow.
| Field | Type | Description |
|---|---|---|
name |
str |
State name (lowercase, alphanumeric + underscore, max 64 chars) |
category |
StateCategory |
One of "open", "wip", "done" |
A valid state transition with enforcement level and field requirements.
| Field | Type | Default | Description |
|---|---|---|---|
from_state |
str |
(required) | Source state |
to_state |
str |
(required) | Target state |
enforcement |
EnforcementLevel |
(required) | "hard" (blocks transition) or "soft" (warns only) |
requires_fields |
tuple[str, ...] |
() |
Fields that must be populated for this transition |
Schema for a custom field on an issue type.
| Field | Type | Default | Description |
|---|---|---|---|
name |
str |
(required) | Field name |
type |
FieldType |
(required) | One of "text", "enum", "number", "date", "list", "boolean" |
description |
str |
"" |
Human-readable description |
options |
tuple[str, ...] |
() |
Valid values for "enum" fields |
default |
Any |
None |
Default value |
required_at |
tuple[str, ...] |
() |
States at which this field must be populated |
Complete workflow definition for an issue type.
| Field | Type | Default | Description |
|---|---|---|---|
type |
str |
(required) | Type identifier |
display_name |
str |
(required) | Human-readable name |
description |
str |
(required) | Type description |
pack |
str |
(required) | Workflow pack this type belongs to |
states |
tuple[StateDefinition, ...] |
(required) | All states in the workflow |
initial_state |
str |
(required) | State for newly created issues |
transitions |
tuple[TransitionDefinition, ...] |
(required) | Valid forward state transitions |
fields_schema |
tuple[FieldSchema, ...] |
(required) | Custom fields for this type |
reverse_transitions |
tuple[TransitionDefinition, ...] |
() |
Controlled escape transitions used by reopen, release revert, and forced close paths |
suggested_children |
tuple[str, ...] |
() |
Suggested child issue types |
suggested_labels |
tuple[str, ...] |
() |
Suggested labels |
A bundle of related type templates.
| Field | Type | Default | Description |
|---|---|---|---|
pack |
str |
(required) | Pack identifier |
version |
str |
(required) | Semantic version |
display_name |
str |
(required) | Human-readable name |
description |
str |
(required) | Pack description |
types |
dict[str, TypeTemplate] |
(required) | Type templates in this pack |
requires_packs |
tuple[str, ...] |
(required) | Pack dependencies |
relationships |
tuple[dict[str, Any], ...] |
(required) | Intra-pack type relationships |
cross_pack_relationships |
tuple[dict[str, Any], ...] |
(required) | Cross-pack type relationships |
guide |
dict[str, Any] | None |
(required) | Workflow guide content (state diagram, tips) |
Result of validating a specific state transition.
| Field | Type | Description |
|---|---|---|
allowed |
bool |
Whether the transition is permitted |
enforcement |
EnforcementLevel | None |
"hard", "soft", or None (unknown transition) |
missing_fields |
tuple[str, ...] |
Fields required but not populated |
warnings |
tuple[str, ...] |
Warning messages (e.g. soft enforcement advisories) |
A possible next state from the current state.
| Field | Type | Description |
|---|---|---|
to |
str |
Target state name |
category |
StateCategory |
Target state category |
enforcement |
EnforcementLevel | None |
Enforcement level for this transition |
requires_fields |
tuple[str, ...] |
All fields required for this transition |
missing_fields |
tuple[str, ...] |
Required fields not yet populated |
ready |
bool |
True if all required fields are populated |
Result of validating an issue against its template.
| Field | Type | Description |
|---|---|---|
valid |
bool |
Whether the issue passes validation |
warnings |
tuple[str, ...] |
Advisory messages for missing recommended fields |
errors |
tuple[str, ...] |
Hard validation errors |
StateCategory = Literal["open", "wip", "done"]
EnforcementLevel = Literal["hard", "soft"]
FieldType = Literal["text", "enum", "number", "date", "list", "boolean"]from filigree.templates import TransitionNotAllowedError, HardEnforcementErrorSubclass of ValueError. Raised when a transition is not defined in the type's transition table.
class TransitionNotAllowedError(ValueError):
from_state: str
to_state: str
type_name: strSubclass of ValueError. Raised when a hard-enforced transition fails field validation.
class HardEnforcementError(ValueError):
from_state: str
to_state: str
type_name: str
missing_fields: list[str]from filigree.core import find_filigree_root, read_config, write_configdef find_filigree_root(start: Path | None = None) -> PathWalks up from start (default: current working directory) looking for a .filigree/ directory.
Returns: The .filigree/ directory path (not the project root).
Raises: FileNotFoundError if no .filigree/ directory is found.
def read_config(filigree_dir: Path) -> dict[str, Any]Reads .filigree/config.json. Returns defaults ({"prefix": "filigree", "version": 1, "enabled_packs": ["core", "planning", "release"]}) if the file is missing.
def write_config(filigree_dir: Path, config: dict[str, Any]) -> NoneWrites a config dict to .filigree/config.json.
from filigree.analytics import cycle_time, lead_time, get_flow_metricsFlow metrics derived from event history. All functions operate read-only on a FiligreeDB instance.
def cycle_time(db: FiligreeDB, issue_id: str) -> float | NoneComputes cycle time in hours: time from the first in_progress status to closed.
Returns: Hours as a float, or None if the issue has not completed the in_progress-to-closed cycle.
def lead_time(db: FiligreeDB, issue_id: str) -> float | NoneComputes lead time in hours: time from issue creation to close.
Returns: Hours as a float, or None if the issue is not closed.
def get_flow_metrics(db: FiligreeDB, *, days: int = 30) -> dict[str, Any]Computes aggregate flow metrics for issues closed within the last days days.
Returns:
{
"period_days": int,
"throughput": int, # Number of issues closed in the period
"avg_cycle_time_hours": float | None, # Average cycle time, or None if no data
"avg_lead_time_hours": float | None, # Average lead time, or None if no data
"by_type": {
"task": {"avg_cycle_time_hours": float, "count": int},
...
},
}