Skip to main content

Schema enforcement and CRUD integrity layer for Obsidian vault folders

Project description

Sigil

Schema enforcement and CRUD integrity layer for Obsidian vault folders โ€” built for agents and humans.

License: MIT PyPI Python

Sigil


Sigil came to life when I saw the notification that the backend I was using for structured data was going to stop working. I realized that that old backend had never really worked decently at all. Half of the time it worked well, the rest of the time, all sorts of chaos happened. It annoyed me at every contact point, and ended up more of a nuisance than a help. Yet, it did fix an important point for me โ€” I need structured data, where the agents working with it can't just forget about the structure and make a big inconsistent mess.

I am, like we all are, a big fan of Obsidian vaults, and on a friction level, those really work best for me. It's accessible, readable, and it has that exciting Bases view, that just looks like it'll do magic. Obsidian is spread over all my devices though, so the ample plugins weren't exactly an easy answer either.

And the answer is ultimately simple โ€” I just need schema enforcement; an integrity layer for when the agents are writing to my Obsidian vault. (And if I make a mess, they can fix it too.) And I like building things ๐Ÿ™‚ So I wrote a briefing, and half a day of agents running, we now have Sigil ready for all of us to escape the dread of having to use anything other than Obsidian. Point your agents here, and enjoy the structure! (And join us in dev and debugging, this is very much an alive project!)

โ€” Gert Schepens


$ sigil check projects/

โœ“ projects/ โ€” 12 files checked

  โœ— acme-proposal.md
    ยท Status: missing (required)
    ยท Created: invalid format โ€” expected ISO 8601, got "June 3"

  โœ— old-contact.md
    ยท Title: missing (required)

2 files with violations ยท 10 files clean

Features

  • Schema-enforced CRUD โ€” create, read, update, delete all validate against the folder's schema. Constraints are enforced at write time โ€” invalid input is rejected before anything is written to disk.
  • 13 field types โ€” string, text, integer, float, boolean, date, datetime, enum, url, email, list, tags, reference
  • sigil check โ€” full folder audit with violation reports, proposed fixes, and --fix for auto-repair
  • sigil move โ€” inter-folder workflow pipeline; destination schema is the sole gate
  • sigil migrate โ€” evolve a schema and rewrite existing files in bulk: add fields, rename values, change required/optional
  • sigil import โ€” bulk-create from a JSON array or CSV file, with per-record constraint enforcement
  • auto defaults โ€” "auto": "today" or "now" fills date/datetime fields at create time without prompting
  • Final states โ€” mark enum values as terminal; sigil read --final and sigil stats surface them
  • Filename templates โ€” "{Date:%y%j}_{Title.slug}" generates consistent, sanitized filenames โ€” including inline date format specs
  • sigil stats โ€” field completion counts, enum distributions, active/final breakdown
  • sigil sources โ€” multi-vault registry with --health, --discover, and --prune
  • sigil logs โ€” scan debug logs for command usage, error patterns, and agent vs human usage split
  • Obsidian Bases โ€” auto-generates a scoped .base view file on sigil init
  • Agent-ready JSON โ€” every command has --json; first daily call includes _sigil hints for command discovery

Install

pip install sigil-obsidian

Requires Python 3.11+.


Quick start

sigil init /path/to/vault/projects   # guided schema setup
sigil check projects/                # audit all files
sigil check projects/ --fix          # auto-repair violations
sigil schema projects/               # inspect the schema
sigil create projects/               # create a validated file
sigil read projects/ --json          # structured output for agents

Commands

Discovery and audit

sigil sources                        # list all known vaults and managed folders
sigil sources --json                 # machine-readable output
sigil sources --health               # add violation count per folder
sigil sources --discover             # find Sigil folders not yet registered
sigil sources --prune                # remove missing paths from config

sigil check <folder>                 # audit all files, list violations
sigil check <folder> --fix           # auto-apply non-destructive fixes
sigil check <folder> --json          # machine-readable output
sigil check <file>                   # validate a single file

sigil schema <folder>                # show required fields, types, options
sigil schema <folder> --json
sigil stats <folder>                 # counts, distributions, finals breakdown
sigil stats <folder> --json
sigil logs                           # scan debug logs โ€” usage, errors, json/human split

CRUD

sigil create <folder>                              # interactive field prompts
sigil create <folder> --data '{"Title": "..."}'    # pass fields directly
sigil create <folder> --schema invoices            # pick named schema (multi-schema folders)

sigil read <folder>                                # list all records
sigil read <folder> --final                        # only records in a final state
sigil read <folder> --json                         # machine-readable
sigil read <folder> <file>                         # single file

sigil update <folder> <file> --data '{"Status": "Done"}'
sigil update <folder> <file> --data '{}' --schema invoices   # pick named schema
sigil delete <folder> <file>

All write operations (create, update, import) enforce the full schema at the point of write โ€” constraints, patterns, min/max, conditional rules, and uniqueness. Invalid data is rejected before anything touches disk.

Bulk operations

sigil import <folder> records.json          # JSON array of objects
sigil import <folder> records.csv           # CSV with header row
sigil import <folder> records.json --skip-errors   # continue on invalid rows

Workflows

sigil move <folder> <file>            # move file to linked workflow folder

Blocked if the file doesn't satisfy the destination schema. No state list needed โ€” the destination schema is the sole gate.

Schema evolution

sigil migrate add-field <folder> <name> --type <type> --default <value>
sigil migrate rename-value <folder> <field> <old> <new>
sigil migrate set-required <folder> <field>
sigil migrate set-optional <folder> <field>

Updates the schema and rewrites all existing files in bulk. rename-value also updates finals if the renamed value was terminal.

Obsidian

sigil base <folder>                   # regenerate .base file (run after moving a folder)
sigil install-skill [<dir>]           # copy skill.md to your agent skills directory
sigil install-cron [<dir>]            # copy cron.md (scheduled integrity check)

Schema file

Each managed folder has a sigil.schema.md file. The schema is a JSON block inside a Markdown file, so it's human-readable in Obsidian and machine-parseable by Sigil:

---
sigil: https://gitlab.com/DigitalGert/sigil
sigil_version: 1.9.7
sigil_install: pip install sigil-obsidian
sigil_skill: https://gitlab.com/DigitalGert/sigil/-/raw/main/skill.md
---

This folder is managed by Sigil ...

\```json
{
  "filename": "{Date}_{Title.slug}",
  "ignore": ["README.md", "*.template.md"],
  "fields": {
    "Title":   { "type": "string",   "required": true,  "obsidian_property": true,
                 "purpose": "Short name for the record" },
    "Status":  { "type": "enum",     "required": true,  "obsidian_property": true,
                 "values": ["Idea", "Drafting", "Published", "Shelved"],
                 "finals": ["Published", "Shelved"],
                 "default": "Idea" },
    "Date":    { "type": "date",     "required": true,  "obsidian_property": true,
                 "auto": "today" },
    "Tags":    { "type": "tags",     "required": false, "obsidian_property": true },
    "Content": { "type": "text",     "required": false }
  },
  "workflow": {
    "links": "communications/published",
    "description": "When a draft reaches Status: Published, move it here with sigil move."
  }
}
\```

Field options

Option Description
type Field type โ€” see table below
required true โ†’ violation if absent
obsidian_property true โ†’ YAML frontmatter; false โ†’ ## FieldName body section
values Enum values array (required for enum type)
finals Subset of values that represent a terminal/done state
default Static default written at create time and by check --fix when absent
auto "today" or "now" โ€” auto-fills a date/datetime field at create time
purpose Human/agent-readable description, shown by sigil schema
pattern Regex the value must fully match โ€” string, text, url, email, reference
minLength / maxLength Character length bounds โ€” string types
min / max Numeric bounds (integer, float) or date bounds ISO string (date, datetime)
minItems / maxItems Item count bounds โ€” list, tags
contains Values that must all be present โ€” list, tags
unique Value must be unique across all records in the folder
warn Soft requirement โ€” reported as โš  warning instead of โœ— error

Field types

Type Description
string Single-line text
text Multiline text โ€” stored as ## FieldName body section unless obsidian_property: true
integer Whole number
float Decimal number
boolean true / false
date ISO 8601 date โ€” YYYY-MM-DD
datetime ISO 8601 datetime โ€” YYYY-MM-DDThh:mm:ss
enum Fixed set of values โ€” requires a values array
url Valid URL (http:// or https://)
email Valid email address
list Array of strings
tags Obsidian-style tags โ€” array
reference Obsidian [[wikilink]] to another note

Filename templates

The filename key controls how new files are named:

"filename": "{Date}_{Title.slug}"
Token Result
{FieldName} Field value, filesystem-sanitized (strips `/:*?"<>
{FieldName.slug} Lowercase-hyphenated slug: "My Great Post" โ†’ my-great-post
{DateField} Date fields render as YYYY-MM-DD by default, or per date_format in the field definition
{DateField:strftime_format} Inline date format override โ€” e.g. {Date:%y%j} renders as 26085 (two-digit year + day-of-year)

Inline format specs let you derive a filename token from an existing date field without storing a separate derived field:

"filename": "{Date:%y%j}_{Title}"

Date: 2026-03-26 โ†’ filename prefix 26085_. The format is applied only to the filename โ€” the stored Date value is unchanged.

  • Stems are truncated to 252 characters before .md is appended (filesystem limit)
  • If a required token has no value, the rename is skipped rather than producing a broken name

Ignore patterns

"ignore": ["README.md", "*.template.md"]

Files matching any pattern are skipped in sigil check, sigil read, and sigil stats.

Workflow

"workflow": {
  "links": "communications/published",
  "description": "Approved drafts move here. Platform must be set."
}

Declares a pipeline to another folder. sigil move <folder> <file> validates the file against the destination schema โ€” if it passes, the file moves; if not, violations are listed and nothing changes. No state list is maintained in the source schema โ€” the destination schema is the sole gate.

Cross-field constraints

"cross_field": [
  {"field_a": "Start", "operator": "<", "field_b": "End", "message": "Start must be before End"}
]

Compares two field values using ==, !=, <, >, <=, >=. Dates and numbers are coerced for comparison. Skipped if either field is absent. message is optional โ€” a default is generated from the rule.

Conditional validation

"conditional": [
  {
    "when": {"field": "Status", "operator": "==", "value": "active"},
    "then": {"field": "Deadline", "required": true}
  }
]

When the when condition is met, the then clause overrides the named field's constraints for that record (required, min, max, minLength, maxLength, pattern, etc.). When the condition is not met, the field's validation is skipped entirely. Use this to express "Deadline is optional for drafts, required when active."

Write-time constraint enforcement

All constraints defined in the schema are enforced at sigil create, sigil update, and sigil import โ€” not only at sigil check. Writes that would produce invalid files are rejected before anything touches disk:

  • Required field missing โ†’ error, write blocked
  • Pattern mismatch, min/max violation, length bounds โ†’ error, write blocked
  • Conditional rule triggered (e.g. PaidDate required when Paid is true) โ†’ error if not satisfied
  • Unique constraint violated โ†’ error listing the conflicting file
  • warn: true fields โ†’ violation printed as warning, write proceeds

Multi-schema folders

Note: one schema per folder is the primary and recommended pattern. Multi-schema is supported for cases where a single folder genuinely holds distinct record types that cannot be separated. Don't reach for it by default.

A single sigil.schema.md can contain multiple schemas, each with an optional match selector. Sigil applies the matching schema(s) per file during check, and you pick a schema by name at create/update time.

{
  "schemas": [
    {
      "name": "invoices",
      "match": {"tags": ["invoice"]},
      "filename": "{Code}_{Title}",
      "fields": {
        "Code":   {"type": "string", "required": true, "obsidian_property": true,
                   "pattern": "INV-\\d{4}", "unique": true},
        "Title":  {"type": "string", "required": true, "obsidian_property": true},
        "Amount": {"type": "float",  "required": true, "obsidian_property": true, "min": 0}
      }
    },
    {
      "name": "notes",
      "match": {"tags": ["note"]},
      "fields": {
        "Title": {"type": "string", "required": true, "obsidian_property": true}
      }
    }
  ]
}

Match selectors

Each schema can have an optional match object with tags, file, or both:

Selector Example Behaviour
tags {"tags": ["invoice"]} Matches files whose frontmatter tags list contains any of the listed tags
file {"file": "INV-*.md"} Matches files whose name matches the glob
Both {"tags": ["invoice"], "file": "INV-*.md"} OR semantics โ€” either match fires
Absent (no match key) Applies to all files (default/fallback schema)
  • Tag matching is case-insensitive and checks tags: / Tags: / TAGS: frontmatter keys
  • A file with no matching schema is silently skipped during sigil check
  • In multi-schema mode, unique is scoped per schema (not folder-wide)
  • Conditional rules and cross-field constraints are per-schema

Creating and updating in multi-schema folders

sigil create <folder> --schema invoices    # use the "invoices" schema
sigil update <folder> <file> --schema notes

If all schemas have matchers and --schema is omitted, Sigil exits with an error listing available schema names. If one schema has no match (matcherless), it acts as the default and --schema can be omitted.

Reading multi-schema folders

sigil read without --schema runs in raw mode โ€” it reads every file in the folder, shows all their actual frontmatter fields as dynamic columns, and lets --where filter on any field name regardless of which schema it belongs to:

sigil read <folder>                          # all files, all fields, dynamic columns
sigil read <folder> --where Title=Foo        # filter across all schemas by any field
sigil read <folder> --where Status=Done --json

Each JSON record includes _schema: "invoices" (or whichever schema matched) so consumers can tell file types apart.

Use --schema to scope to one schema's typed view:

sigil read <folder> --schema invoices        # only invoice files, invoice columns
sigil read <folder> --schema invoices --where Code=INV-0001

Viewing the schema definition

sigil schema <folder>        # shows each schema with its match header
sigil schema <folder> --json # {"schemas": [...]} with full field definitions

Bulk import in multi-schema folders

sigil import always uses the primary (matcherless) schema. If your folder has only named schemas (all with matchers), import is not supported โ€” create files individually with sigil create --schema <name> instead.


check --fix auto-repairs

sigil check --fix applies non-destructive fixes without human input:

Violation Fix applied
Body field found in YAML frontmatter Moves to ## FieldName body section
Enum value with one fuzzy match Corrects automatically
Filename doesn't match template Renames the file
sigil_schema breadcrumb missing or stale Adds / updates it
Required field absent โ€” has a default Inserts the default value
Multiple fuzzy enum matches Lists candidates โ€” human or agent decides

Breadcrumbs

Every file created or updated by Sigil gets sigil_schema in its YAML frontmatter, pointing to the schema file that governs it:

---
sigil_schema: communications/drafts/sigil.schema.md
Title: Why I Switched to Obsidian
Status: Drafting
Date: 2026-06-21
---

This lets agents identify Sigil-managed files cold and locate the schema without a config lookup. If the breadcrumb is missing or stale, sigil check --fix corrects it.


Agent integration

Sigil is a general-purpose CLI โ€” it works from any agent that can run shell commands. No platform lock-in.

skill.md

Sigil ships a compact command reference (skill.md) that any agent can load as context:

sigil install-skill                             # install to default skills directory
sigil install-skill ~/.openclaw/skills/         # custom path
sigil install-skill                             # omit path to preview location first

Or install manually:

curl -o ~/.claude/skills/sigil.md \
  https://gitlab.com/DigitalGert/sigil/-/raw/main/skill.md

_sigil hints in JSON output

The first --json call of the day for each command type includes a _sigil key with ready-to-run commands for every available operation on that folder:

{
  "folder": "/path/to/drafts",
  "total_violations": 0,
  "_sigil": {
    "hints": {
      "create": "sigil create \"/path/to/drafts\" --data '{\"Title\": \"...\", \"Status\": \"Idea\"}'",
      "read":   "sigil read \"/path/to/drafts\" --json",
      "move":   "sigil move \"/path/to/drafts\" <file>  # โ†’ communications/published",
      "import": "sigil import \"/path/to/drafts\" records.json  # or records.csv",
      "logs":   "sigil logs  # available when debug logging is enabled in ~/.sigil.config"
    },
    "workflow_next": "communications/published",
    "workflow_description": "When a draft reaches Status: Published..."
  }
}

Hints are suppressed on repeat calls the same day (tracked in ~/.sigil.config under hints_shown). When a folder has a workflow declared, move, workflow_next, and workflow_description are always included. Check "_sigil" in response before accessing.

Isolated config for agent environments

Point SIGIL_CONFIG to a path inside your skills directory to keep Sigil's state portable and separate from your personal config:

export SIGIL_CONFIG=~/.claude/skills/sigil.config

cron.md โ€” scheduled integrity checks

Sigil ships a cron skill file for scheduled vault integrity checks:

sigil install-cron ~/.claude/crons/

Config

Global config lives at ~/.sigil.config (YAML). Created on first sigil init.

vaults:
  - path: /home/user/Documents/Dara
    name: Dara
managed_folders:
  - path: /home/user/Documents/Dara/communications/drafts
    schema: custom
    initialized: '2026-06-21'
hints_shown:
  check: '2026-06-21'
  read:  '2026-06-21'

Override the config path:

export SIGIL_CONFIG=/path/to/config     # environment variable
sigil --config /path/to/config ...      # per-command flag

Debug logging

debug: true
log_path: ~/sigil-logs/

When enabled, Sigil writes dated log files (sigil-YYYY-MM-DD.log) to log_path. sigil logs scans them and reports command usage, error counts, and the JSON vs human output-mode split โ€” useful for understanding how agents are using the tool.


Exit codes

Code Meaning
0 Clean โ€” no violations
1 Violations found
2 Tool error

Built-in schema templates

sigil init <folder> offers four templates:

Template Fields
task Title, Status (Open/In Progress/Done/Cancelled), Priority, Created, Tags, Description
contact Name, Email, Phone, Relationship, Category, Tags, Notes
note Title, Date, Tags, Updated, Content
project Title, Status, Start, End, Tags, Description

Pass --template task to skip the interactive prompt.


Contributing

This is an active project. Bug reports, schema template ideas, and pull requests are welcome.

Development setup

git clone https://gitlab.com/DigitalGert/sigil.git
cd sigil
pip install -e ".[dev]"
python -m pytest tests/
python -m ruff check src/

The test suite has 415 tests covering all flows, all 13 field types, schema parsing, CRUD edge cases, workflow moves, migrations, imports, health checks, the hint system, multi-schema matching, raw-mode read, constraint enforcement at write time, and CLI-layer error paths.

Versioning

Sigil follows SemVer. Breaking changes (removed commands, changed JSON keys) increment the minor version. The sigil_version field in each schema file records the Sigil version that last wrote it โ€” sigil check warns when the installed version lags behind.


License

MIT โ€” see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sigil_obsidian-1.9.7.tar.gz (92.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sigil_obsidian-1.9.7-py3-none-any.whl (65.7 kB view details)

Uploaded Python 3

File details

Details for the file sigil_obsidian-1.9.7.tar.gz.

File metadata

  • Download URL: sigil_obsidian-1.9.7.tar.gz
  • Upload date:
  • Size: 92.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for sigil_obsidian-1.9.7.tar.gz
Algorithm Hash digest
SHA256 d12748d1af2f5653b338a6817e238caa6e88f6e681f345ea2a6acef25e0167c7
MD5 c84b6180bcd841b9daa8e6257f10d7a9
BLAKE2b-256 7d8bde2412576f754799c39af307fe59308b9d50d291295c81417f39ecc38baa

See more details on using hashes here.

File details

Details for the file sigil_obsidian-1.9.7-py3-none-any.whl.

File metadata

  • Download URL: sigil_obsidian-1.9.7-py3-none-any.whl
  • Upload date:
  • Size: 65.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for sigil_obsidian-1.9.7-py3-none-any.whl
Algorithm Hash digest
SHA256 613d0c2abacb7cd7a4e9fa6baa5fbc57cf38fa1c8dca7119de9b86c79c86d313
MD5 c6cc72785b4a485b3f5ca108f72c03dd
BLAKE2b-256 df7cec241be724fa39601446cf94c96ee4a114fccc294ef7df4d7fcd1cfddee7

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page