Skip to content

Ductile: Database Reference

Ductile uses SQLite 3 for all persistent state, job queuing, and execution history. This document provides the schema definitions and a collection of useful queries for operators.


Database Location

The database is typically named ductile.db and resides in your configured state.path (default: ~/.config/ductile/ductile.db).


Schema Overview

Fact Rows vs Current Rows

Ductile keeps current/cache rows for fast operational reads and append-only fact rows for durable explanation.

Fact/history rows (durable record): - plugin_facts - job_transitions - job_attempts - config_snapshots - event_context - job_log - circuit_breaker_transitions

Current/cache rows (derived views and operational state): - job_queue - plugin_state (compatibility view of the latest plugin_facts row per plugin) - circuit_breakers - schedule_entries

storage_sequences is an internal allocator for Ductile-owned fact ordering. New plugin_facts rows receive a monotonic seq. Legacy plugin_facts rows may have seq IS NULL; those rows keep their old timestamp-only ordering and should not be treated as perfectly ordered facts.

1. job_queue

The active work queue. Contains pending, running, and recently completed jobs.

Column Type Description
id UUID Unique job identifier.
status TEXT queued, running, succeeded, failed, timed_out, dead.
plugin TEXT Name of the plugin or alias.
command TEXT The plugin command (e.g., poll, handle).
payload JSON Input data for the plugin.
dedupe_key TEXT Used to prevent duplicate enqueues.
event_context_id UUID Reference to the baggage/context for this job.

2. job_log

The historical record of completed jobs. Used for auditing and the TUI "Overwatch."

Column Type Description
result JSON The full protocol response from the plugin.
stderr TEXT Captured stderr (capped at 64 KB).
last_error TEXT Human-readable error message if the job failed.

3. event_context

The "Control Plane" ledger. Stores metadata (Baggage) that propagates through pipelines.

4. plugin_facts

Append-only durable record of plugin observations. Each row carries a stable snapshot a plugin emitted as state_updates, plus a manifest-declared fact_type and a Ductile-owned monotonic seq. This is the durable plugin record. New plugins should declare fact_outputs and emit a snapshot from their durable command.

5. plugin_state

Compatibility/cache view of the latest fact, one row per plugin. Existing readers and legacy plugins still on direct write-through see the same shape they always have. The view is rebuilt automatically by core when a new fact lands, governed by the manifest's compatibility_view (currently mirror_object).

For existing databases, apply required schema migrations before deploy. Startup validation reports the migration script to run when the database is behind the runtime schema.

6. schedule_entries

The persistent state of the scheduler. Tracks when each schedule last fired and when it is due next.

7. circuit_breakers

Current-state compatibility/cache row for scheduled poll circuit breakers.

8. circuit_breaker_transitions

Append-only transition facts for circuit breakers. Use this table to explain why a breaker opened, moved half-open, closed, or was manually reset.


Useful Operator Queries

System Health

-- Count jobs by status
SELECT status, COUNT(*) 
FROM job_queue 
GROUP BY status;

-- Identify plugins with active circuit breakers
SELECT plugin, command, state, failure_count, opened_at 
FROM circuit_breakers 
WHERE state != 'closed';

-- Show recent breaker transitions
SELECT created_at, plugin, command, from_state, to_state, reason, failure_count, job_id
FROM circuit_breaker_transitions
WHERE plugin = 'my-plugin' AND command = 'poll'
ORDER BY created_at DESC
LIMIT 20;

-- Check for stuck "running" jobs (orphans)
SELECT id, plugin, command, started_at 
FROM job_queue 
WHERE status = 'running' 
  AND started_at < datetime('now', '-1 hour');

Performance & Troubleshooting

-- Find the slowest successful jobs in the last 24 hours
SELECT plugin, command, 
       (strftime('%s', completed_at) - strftime('%s', started_at)) as duration_sec
FROM job_log
WHERE status = 'succeeded'
  AND completed_at > datetime('now', '-1 day')
ORDER BY duration_sec DESC
LIMIT 10;

-- Get the latest error for a specific plugin
SELECT completed_at, last_error, stderr
FROM job_log
WHERE plugin = 'my-plugin' AND status = 'failed'
ORDER BY completed_at DESC
LIMIT 1;

-- Inspect recent append-only plugin facts (the durable record)
SELECT seq, created_at, fact_type, job_id, command, fact_json
FROM plugin_facts
WHERE plugin_name = 'file_watch'
ORDER BY
  CASE WHEN seq IS NULL THEN 1 ELSE 0 END ASC,
  seq DESC,
  created_at DESC
LIMIT 20;

-- Inspect a plugin's compatibility view (latest fact, mirrored)
SELECT state FROM plugin_state WHERE plugin_name = 'my-plugin';

Scheduler Inspection

-- See upcoming scheduled runs
SELECT plugin, schedule_id, next_run_at, last_success_at
FROM schedule_entries
WHERE status = 'active'
ORDER BY next_run_at ASC;

Maintenance

Manual Cleanup

Ductile automatically prunes job_log after 30 days, but you can manually vacuum or prune if needed:

# Prune logs older than 7 days
sqlite3 ductile.db "DELETE FROM job_log WHERE completed_at < datetime('now', '-7 days');"

# Reclaim disk space
sqlite3 ductile.db "VACUUM;"

Performance Tuning

Ductile enables WAL mode and Synchronous=NORMAL by default for optimal performance on SSDs. You can verify this via:

PRAGMA journal_mode;
PRAGMA synchronous;