CLI interface¶
Installing the package also installs a synalog command (also available as python -m synalog). Run synalog --help for the full usage.
If you use uv, uvx runs the CLI in an ephemeral environment without installing anything:
uvx synalog # interactive session
uvx --from 'synalog[run]' synalog program.l run Total # adds the psycopg (psql) driver
Plain uvx synalog covers print and execution on duckdb (the default engine, bundled with synalog) and sqlite (Python's built-in driver); executing on PostgreSQL needs the psycopg driver from the synalog[run] extra, hence --from.
One-shot commands¶
The argument order follows logica: the program file first, then the command.
synalog program.l print Predicate ... # print compiled SQL
synalog program.l run Predicate ... # execute and print a table
synalog program.l run Predicate --csv # execute and print CSV
Both print and run validate the whole program first and exit 1 with the verifier's errors if it is invalid, so there is no separate check step to forget. print and run accept several predicate names and process them in order. In a terminal, print highlights the SQL and run renders the table with rich formatting; piped output falls back to plain text. Add --csv to run for machine-readable CSV instead of the rendered table.
$ synalog program.l run EngineeringTeam
+---------+--------+
| name | salary |
+---------+--------+
| Alice | 75000 |
| Charlie | 80000 |
+---------+--------+
2 rows
Options¶
| Option | Meaning |
|---|---|
-c PROGRAM |
Pass the program text inline instead of FILE, like python -c. |
--engine <name> |
Target SQL dialect. Resolution order: this flag, then the program's @Engine annotation, then duckdb. |
--limit N / --offset N |
Paginate the result. |
--csv |
With run: print results as CSV instead of a rendered table. |
--search REGEX |
With print/run: keep only rows where some column matches the regular expression REGEX (engine-native regex, not a SQL LIKE pattern). Applies to the filtered rows before pagination. |
--import-root DIR |
Directory where import statements look up .l files (repeatable). |
--load TABLE=PATH |
Load a csv/tsv/json/jsonl/parquet file as a table before running (repeatable). |
--dsn <conninfo> |
PostgreSQL connection string for --engine psql (or set SYNALOG_PSQL_DSN). |
Passing - as the file reads the program from stdin, and -c takes the program text directly, and both compose with the other options:
echo 'Greeting("hi");' | synalog - print Greeting
synalog -c 'Greeting("hi");' run Greeting
synalog -c 'Digit(d) :- d in [1, 2, 3];' run Digit --limit 2 --offset 1
With -c there is no FILE argument: the positionals are the command and its predicates.
Executing locally¶
run (optionally with --csv) executes the compiled SQL in-process:
- duckdb: the default engine, bundled with synalog; nothing extra to install.
- sqlite: Python's stdlib driver; Logica's runtime UDFs (ArgMin/ArgMax, ARRAY_CONCAT, ...) are registered when the
logicapackage is installed. - psql: needs
pip install psycopgand a connection string.
The Today and Now built-in concepts need no runner support, since the compiler inlines them per dialect, so they work on every engine.
Since connections are in-memory and per-run, --load is how you bring data in: each TABLE=PATH pair is loaded before the script runs, and the program refers to it by the table name. duckdb reads csv/tsv/json/jsonl/parquet natively; the sqlite runner parses csv/tsv/json/jsonl in Python (no parquet); the psql runner cannot load files.
$ synalog totals.l run Total --load sales=sales.csv
+--------+-------+
| region | total |
+--------+-------+
| north | 15 |
| south | 20 |
+--------+-------+
2 rows
duckdb ships with synalog; pip install 'synalog[run]' adds the psycopg driver for PostgreSQL. For the other engines (bigquery, trino, presto, databricks), use print and run the SQL with your own client.
Imports¶
import path.to.file.Pred; statements resolve path/to/file.l against the program file's directory, then the current directory. Pass --import-root DIR (repeatable) to search elsewhere; explicit roots replace the defaults.
For example, with a reusable metric in lib/metrics.l:
@OrderBy(RegionTotal, "region");
RegionTotal(region:, total? += amount) distinct :- sales(region:, amount:);
a program next to the lib/ directory imports it by its dotted path and builds on it:
# report.l
import lib.metrics.RegionTotal;
@OrderBy(TopRegion, "total DESC");
@Limit(TopRegion, 1);
TopRegion(region:, total:) :- RegionTotal(region:, total:);
$ synalog report.l run TopRegion --load sales=sales.csv
+--------+-------+
| region | total |
+--------+-------+
| south | 20 |
+--------+-------+
1 row
import lib.metrics.RegionTotal as Totals; imports the same predicate under another name. Directives attached to an imported predicate (its @OrderBy here) travel with it.
Errors¶
Errors go to stderr (shown in red in a terminal) and exit with code 1; usage mistakes (unknown option, missing argument) exit with code 2. There are three layers, surfaced in the order the program is processed:
Syntax errors come from the parser. Both commands report them the same way, quoting the offending statement with a marker at the position where parsing stopped:
$ synalog -c 'Bad(x) :- x ==;' print Bad
Parsing:
Bad(x) :- x ==<EMPTY>
[ Error ] Could not parse expression of a value.
$ echo $?
1
Verification errors come from the formal verifier, which print and run run over the whole program before compiling, reporting all problems at once, one per line:
$ synalog -c 'A(x:, y:) :- B(x:);
C(z:) :- D(w:);' print A
Unbound variable 'y' in head of rule: A(x:, y:) :- B(x:)
Unbound variable 'z' in head of rule: C(z:) :- D(w:)
Compile errors come from print and run when SQL generation fails, for example when the requested predicate does not exist:
$ synalog -c 'Greeting("hi");' run Missing
Compile error: No rules are defining 'Missing', but compilation was requested.
A failing program never produces partial output: run either prints the table or the error.
Add the skill to your coding agent¶
Synalog ships an Agent Skill: a SKILL.md that teaches a coding agent the language, the CLI and the conventions, so it can write and run programs correctly. The skill follows the open Agent Skills standard, so it works with Claude Code, Cursor, Codex, OpenCode, Cline, Windsurf and many other agents.
Install it with the skills CLI (GitHub is the registry, so there is nothing to publish or install first):
$ npx skills add SynaLinks/synalog # this project (./.claude/skills, ./.agents/skills, …)
$ npx skills add SynaLinks/synalog -g # user-wide (~/.claude/skills, …)
$ npx skills add SynaLinks/synalog -a cursor codex # only specific agents
The skill is maintained in the repository at skills/synalog/SKILL.md; npx skills add copies it into the right location for each agent.
Interactive session¶
Running synalog with no arguments starts a REPL, in the spirit of python:
$ synalog
Synalog 0.1.0 on duckdb, type .help for help
>>> Employee(name: "Alice", salary: 75000);
>>> Employee(name: "Bob", salary: 65000);
>>> Total(t? += salary) distinct :- Employee(salary:);
>>> Total
+--------+
| t |
+--------+
| 140000 |
+--------+
1 row
Type a rule ending in ; to add it to the session program. It is validated first and rejected with an error if invalid, leaving the program untouched. A statement can span several lines: the prompt switches to ... until the closing ;. Type a predicate name to compile and run it.
Errors never end the session: the offending input is simply not added, as .show confirms:
>>> Greeting("hi");
>>> Bad(x) :- x ==;
Parsing:
Bad(x) :- x ==<EMPTY>
[ Error ] Could not parse expression of a value.
>>> .show
Greeting("hi");
The --engine, --dsn, --import-root and --load options also apply to the session:
Session commands:
| Command | Meaning |
|---|---|
.help |
Show help. |
.show |
Show the current program. |
.sql <Pred> |
Print the SQL compiled for a predicate. |
.search <Pred> <regex> |
Run <Pred>, keeping only rows where some column matches the regular expression. |
.engine <name> |
Switch engine. |
.load <table> <path> |
Load a csv/tsv/json/jsonl/parquet file as a table. |
.clear |
Discard the program and loaded tables. |
.exit |
Leave (also .quit or Ctrl+D). |