[{"content":"Hello, World The smallest useful Tln program finds something in your data and flags it. Here we flag any greeting record and greet it by name — Prolog on the left, Tln on the right:\nISO Prolog (1995)% Facts loaded externally; shown here as plain facts: greeting(1, \u0026#39;World\u0026#39;). hello(Id) :- greeting(Id, Name), format(\u0026#34;Hello, ~w!~n\u0026#34;, [Name]). Tlndetect \u0026#34;Hello, World\u0026#34; { for records where type == \u0026#34;greeting\u0026#34; flag matching items label \u0026#34;Hello, {attr.name}!\u0026#34; } Line by line:\ndetect \u0026quot;Hello, World\u0026quot; — a block. detect finds records matching a condition and flags them. for records where type == \u0026quot;greeting\u0026quot; — the selector: which records this block runs over. flag matching items — flag every record that matched. label \u0026quot;Hello, {attr.name}!\u0026quot; — a template; {attr.name} interpolates that record\u0026rsquo;s name. Optional clauses like priority LOW|MEDIUM|HIGH|CRITICAL can tune how loudly a result is surfaced, but they aren\u0026rsquo;t required.\nTest it Tln has a built-in test framework. Facts live in a given block, and you assert on the result:\ntest \u0026#34;greets the world\u0026#34; { given { record 1 type \u0026#34;greeting\u0026#34; attr 1 \u0026#34;name\u0026#34; \u0026#34;World\u0026#34; } when detect \u0026#34;Hello, World\u0026#34; expect { flagged 1 label contains \u0026#34;Hello, World!\u0026#34; count == 1 } } Run it:\ntln test hello.tln hello.tln.test # ==\u0026gt; hello.tln.test: 1 test(s) # # 1 passed, 0 failed The other classic: family trees Every Prolog tutorial has the family tree — a handful of facts and a recursive ancestor. Tln splits this the way production systems already do: the parent relation is data (loaded as facts, never written in .tln) and the derivations are rules:\nISO Prolog (1995)parent(tom, bob). parent(bob, ann). grandparent(X, Z) :- parent(X, Y), parent(Y, Z). ancestor(X, Z) :- parent(X, Z). ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z). Tln// facts: record(family, \u0026#34;bob\u0026#34;, person, ...) // attr(family, \u0026#34;bob\u0026#34;, \u0026#34;parent\u0026#34;, \u0026#34;tom\u0026#34;) detect \u0026#34;Children of Tom\u0026#34; { for records where type == \u0026#34;person\u0026#34; and attr \u0026#34;parent\u0026#34; == \u0026#34;tom\u0026#34; flag matching items label \u0026#34;{item.name} is a child of Tom\u0026#34; } One-hop relationships are ordinary blocks. The transitive ancestor closure — a recursive rule — runs on Tln\u0026rsquo;s recursive Datalog resolver (the same machinery behind the built-in category_tree) and terminates even on cyclic graphs. See Derived predicates \u0026amp; recursion.\nThe mental model Facts are your data — and you never write them in .tln. They are loaded from your existing systems (ERP, CRM, asset management, a warehouse DB, MCP tools…) as generic entity–attribute records:\nrecord(Entity, Id, Type, Category, Status, Date) // the thing attr(Entity, Id, Name, Value) // a property of the thing Rules are your knowledge. You write blocks that reason over those facts:\nBlock Answers define a reusable named condition rule \u0026ldquo;Is this allowed?\u0026rdquo; (allow / block) detect \u0026ldquo;What pattern exists?\u0026rdquo; (flag + label) recommend \u0026ldquo;What should we do next?\u0026rdquo; combine \u0026ldquo;What\u0026rsquo;s the optimal mix?\u0026rdquo; predict forecast classify cluster find built-in ML — see Beyond Prolog workflow on collect enrich act on the world via MCP — see MCP \u0026amp; workflows The engine is native Go. Tln does not interpret rules ad-hoc: a lexer → parser → validator → planner turns each block into a deterministic query plan ([]PlanStep) that an executor runs against a pluggable FactStore. Same facts in, same decision out — every time, and every decision traces back to the exact rule that fired.\nThis is Expert-in-the-Loop: instead of a human (or an LLM) re-deciding every case, a deterministic expert system decides, and people are reserved for the rare case that truly needs them. In production that engine is the decision core of OpenTalon — the LLM handles intent and language, Tln handles knowledge and inference.\nThe CLI tln build rules.tln # parse, validate, and show the query plan tln test rules.tln t.tln.test # run .tln.test assertions tln run rules.tln --seed t.tln.test # evaluate against a FactStore tln explain rules.tln t.tln.test # trace why each result fired tln repl # interactive: :load, :eval, :trace Next: the Language Reference, or see Tln beside the language it modernizes in Prolog → Tln.\n","permalink":"https://tln-lang.org/docs/getting-started/","summary":"\u003ch2 id=\"hello-world\"\u003eHello, World\u003c/h2\u003e\n\u003cp\u003eThe smallest useful Tln program finds something in your data and flags it. Here we flag any\n\u003ccode\u003egreeting\u003c/code\u003e record and greet it by name — Prolog on the left, Tln on the right:\u003c/p\u003e\n\u003cdiv class=\"compare\"\u003e\n  \n\u003cdiv class=\"compare-pane compare-pane--prolog\"\u003e\u003cdiv class=\"compare-pane__title\"\u003eISO Prolog (1995)\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-prolog\" data-lang=\"prolog\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e% Facts loaded externally; shown here as plain facts:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003egreeting\u003c/span\u003e(\u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e, \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;World\u0026#39;\u003c/span\u003e).\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003ehello\u003c/span\u003e(Id) :-\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003egreeting\u003c/span\u003e(Id, Name),\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eformat\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Hello, ~w!~n\u0026#34;\u003c/span\u003e, [Name]).\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\n\u003cdiv class=\"compare-pane compare-pane--tln\"\u003e\u003cdiv class=\"compare-pane__title\"\u003eTln\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-tln\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edetect \u0026#34;Hello, World\u0026#34; {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  for records where type == \u0026#34;greeting\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  flag matching items\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  label \u0026#34;Hello, {attr.name}!\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\n\n\u003c/div\u003e\n\n\u003cp\u003eLine by line:\u003c/p\u003e","title":"Getting Started"},{"content":"An LLM extracts facts from each claim invoice (PDF → record + attrs). The expert system then decides — deterministically — whether to auto-approve, auto-reject, or escalate to a human. No LLM in the decision loop; every outcome traces to a specific rule.\nBlock a blacklisted provider A claim from a blacklisted provider must never be approved — and this decision must win over any \u0026ldquo;auto-approve\u0026rdquo; that also matches. In Prolog you model the decision term and its message yourself; in Tln you block with a reason and a CRITICAL priority that resolves the conflict for you.\nISO Prolog (1995)blacklisted_provider(E, Id) :- record(E, Id, claim, _, _, _), attr(E, Id, provider_status, blacklisted). % Model the decision term and the message by hand. % Conflict resolution vs auto_approve/2 is also on you. claim_decision(E, Id, block(approve_claim), Reason) :- blacklisted_provider(E, Id), attr(E, Id, provider_id, P), format(atom(Reason), \u0026#34;Provider ~w is on the fraud blacklist\u0026#34;, [P]). Tlnrule \u0026#34;Reject blacklisted provider\u0026#34; { for records where type == \u0026#34;claim\u0026#34; and attr \u0026#34;provider_status\u0026#34; == \u0026#34;blacklisted\u0026#34; block \u0026#34;approve_claim\u0026#34; reason \u0026#34;Provider {attr.provider_id} is on the fraud blacklist\u0026#34; } Auto-approve the routine case ISO Prolog (1995)auto_approve(E, Id) :- record(E, Id, claim, _, _, _), attr(E, Id, provider_status, in_network), attr(E, Id, service_category, outpatient), attr(E, Id, amount_chf, Amount), attr(E, Id, per_visit_cap, Cap), Amount =\u0026lt; Cap. Tlnrule \u0026#34;Auto-approve in-network routine\u0026#34; { for records where type == \u0026#34;claim\u0026#34; and attr \u0026#34;provider_status\u0026#34; == \u0026#34;in_network\u0026#34; and attr \u0026#34;service_category\u0026#34; == \u0026#34;outpatient\u0026#34; and attr \u0026#34;amount_chf\u0026#34; \u0026lt;= attr \u0026#34;per_visit_cap\u0026#34; allow \u0026#34;approve_claim\u0026#34; } Surface what needs a human Over-cap claims go to a reviewer. Notice what the Prolog version has to spell out: the query, the forall loop, and the format string. Tln\u0026rsquo;s detect declares the flag, the label, and the priority — and a recommend block can chain straight off it.\nISO Prolog (1995)over_cap(E, Id, Over) :- record(E, Id, claim, _, _, _), attr(E, Id, amount_chf, Amount), attr(E, Id, per_visit_cap, Cap), Amount \u0026gt; Cap, Over is Amount - Cap. % Flagging + labelling is manual plumbing: report_over_cap(E) :- forall(over_cap(E, Id, Over), ( attr(E, Id, amount_chf, Amount), format(\u0026#34;Claim ~w: ~w CHF over cap (by ~w)~n\u0026#34;, [Id, Amount, Over]) )). Tlndetect \u0026#34;Over the per-visit cap\u0026#34; { for records where type == \u0026#34;claim\u0026#34; and attr \u0026#34;amount_chf\u0026#34; \u0026gt; attr \u0026#34;per_visit_cap\u0026#34; flag matching items label \u0026#34;Claim {item.id}: {attr.amount_chf} CHF over the per-visit cap\u0026#34; } recommend \u0026#34;Schedule reviewer\u0026#34; { when detect \u0026#34;Over the per-visit cap\u0026#34; matches suggest \u0026#34;Route claim {item.id} ({attr.amount_chf} CHF) to a senior adjuster\u0026#34; } Why the Tln version wins Declarative outcomes. flag / label / allow / block replace the query-collect-format loop. Conflict resolution is built in. A strict rule or an overrides clause settles competing verdicts — no manual ordering of clauses. It\u0026rsquo;s testable. Drop the facts in a .tln.test given block and assert the decision — see testing. It\u0026rsquo;s explainable. tln explain traces the exact rule and facts behind every outcome. ","permalink":"https://tln-lang.org/comparisons/insurance-claims/","summary":"\u003cp\u003eAn LLM extracts facts from each claim invoice (PDF → \u003ccode\u003erecord\u003c/code\u003e + \u003ccode\u003eattr\u003c/code\u003es). The expert system then\ndecides — deterministically — whether to auto-approve, auto-reject, or escalate to a human. No\nLLM in the decision loop; every outcome traces to a specific rule.\u003c/p\u003e\n\u003ch2 id=\"block-a-blacklisted-provider\"\u003eBlock a blacklisted provider\u003c/h2\u003e\n\u003cp\u003eA claim from a blacklisted provider must never be approved — and this decision must \u003cem\u003ewin\u003c/em\u003e over any\n\u0026ldquo;auto-approve\u0026rdquo; that also matches. In Prolog you model the decision term and its message yourself;\nin Tln you \u003ccode\u003eblock\u003c/code\u003e with a \u003ccode\u003ereason\u003c/code\u003e and a \u003ccode\u003eCRITICAL\u003c/code\u003e priority that resolves the conflict for you.\u003c/p\u003e","title":"Insurance claims auto-adjudication"},{"content":"Prolog is a pure inference engine. It answers queries; it does not do things. There is no standard way to call an external tool, and nothing reactive — a fact changing can\u0026rsquo;t fire an action, because the language has no notion of \u0026ldquo;an action\u0026rdquo; or of \u0026ldquo;changing.\u0026rdquo; In practice you bolt on non-standard extensions and hand-roll all the orchestration yourself.\nTln makes tools and reactivity first-class, through the Model Context Protocol (MCP). This is the capability that turns a ruleset into a running agent — and it\u0026rsquo;s what powers OpenTalon in production.\nReact to a fact change → call a tool When an item\u0026rsquo;s stock hits zero, place a refill order. In Tln an on change block fires a workflow, and a step calls the inventory MCP tool directly. This example is real: examples/refill_agent.\nISO Prolog (1995)% No tool calls, no \u0026#34;on change\u0026#34; trigger. Even with non-standard % SWI extensions, the orchestration is hand-rolled: :- use_module(library(process)). refill(Id) :- process_create(path(inventory_cli), [\u0026#39;create-refill-order\u0026#39;, \u0026#39;--item\u0026#39;, Id, \u0026#39;--qty\u0026#39;, \u0026#39;5\u0026#39;], [process(_)]). % ...and you must notice the stock-out and call refill/1 % yourself. There is no declarative trigger on a fact % changing to 0. Tlnon change attr \u0026#34;current_stock\u0026#34; to 0 { logger.warn \u0026#34;stock-out detected for item {event.entity}\u0026#34; workflow \u0026#34;Refill stock\u0026#34; } workflow \u0026#34;Refill stock\u0026#34; { step \u0026#34;reorder\u0026#34; { tool \u0026#34;inventory\u0026#34; \u0026#34;create-refill-order\u0026#34; { item_id step(\u0026#34;trigger\u0026#34;).result.entity quantity 5 } } } Ingest facts on a schedule Facts come from the outside world — so Tln can pull them in. A collect block declares what to fetch (via MCP) and when; the host fires it on schedule and the results land as facts, ready for rules to reason over. Retries and error handling are declarative too.\ncollect \u0026#34;Failure training data\u0026#34; { schedule weekly tool \u0026#34;inventory\u0026#34; \u0026#34;list-items\u0026#34; { query \u0026#34;status:defective\u0026#34; per_page 100 on_error { retry 3 times then log \u0026#34;collect failed: {error}\u0026#34; then skip } } store results as training_facts tag \u0026#34;failure_training\u0026#34; } Branch and fan out while remediating A detect can carry a remediate body with real control flow — if/else on the matched row, and for each to fan an action across channels — every leaf a tool call:\ndetect \u0026#34;Overdue for service\u0026#34; { for records where type == \u0026#34;vehicle\u0026#34; and attr \u0026#34;km\u0026#34; \u0026gt; attr \u0026#34;last_service_km\u0026#34; + 20000 flag matching items remediate { if attr \u0026#34;priority\u0026#34; == \u0026#34;CRITICAL\u0026#34; { tool \u0026#34;ops\u0026#34; \u0026#34;page_oncall\u0026#34; { vehicle attr \u0026#34;id\u0026#34; severity \u0026#34;critical\u0026#34; } } else { tool \u0026#34;ops\u0026#34; \u0026#34;open_ticket\u0026#34; { vehicle attr \u0026#34;id\u0026#34; reason \u0026#34;overdue service\u0026#34; } } for each channel in [\u0026#34;fleet-ops\u0026#34;, \u0026#34;maintenance\u0026#34;] { tool \u0026#34;slack\u0026#34; \u0026#34;notify\u0026#34; { channel channel text \u0026#34;Vehicle {item.id} overdue for service\u0026#34; } } } } How the tools connect — the plugin system The calls above use the plugin-neutral tool verb — tool \u0026quot;server\u0026quot; \u0026quot;name\u0026quot; {…} — and don\u0026rsquo;t hard-wire any transport. Tln\u0026rsquo;s language core is transport-free: it decides which tool calls fire and returns them as data; a host-injected ToolResolver performs the actual IO. The server name is what routes: tool \u0026quot;inventory\u0026quot; … reaches an MCP server via tln-mcp (the Model Context Protocol over JSON-RPC), while tool \u0026quot;io\u0026quot; \u0026quot;writeln\u0026quot; reaches the built-in io-tln plugin. Neither is baked into the language.\nIn the language you simply name a server and a tool — the transport is the host\u0026rsquo;s concern:\nworkflow \u0026#34;Notify low stock\u0026#34; { step \u0026#34;reorder\u0026#34; { tool \u0026#34;inventory\u0026#34; \u0026#34;create-refill-order\u0026#34; { item_id item.id quantity 5 } } step \u0026#34;announce\u0026#34; { tool \u0026#34;slack\u0026#34; \u0026#34;post-message\u0026#34; { channel \u0026#34;#ops\u0026#34; text \u0026#34;reordered {item.id}\u0026#34; } } } Those tool calls — and collect / enrich / remediate — dispatch to the named server via JSON-RPC tools/call. The host attaches tln-mcp (or a mock in tests, a direct HTTP client, an internal bus) without touching a single rule.\nThis is the tools leg of Tln\u0026rsquo;s plugin model, mirroring tln-db on the storage side: the core is a pure language + planner + SPIs, and every IO edge is a plugin. At the OpenTalon layer the same model adds channels (Slack, HTTP, MS Teams, WebSocket), security (guard-llm), and retrieval/RAG (weaviate).\nThe whole loop The decision stays deterministic and explainable; the acting — paging, ticketing, notifying, reordering — happens through MCP tools the rule names directly. That\u0026rsquo;s the whole loop: facts in, a deterministic decision, and an action out — none of which ISO Prolog can express on its own.\n","permalink":"https://tln-lang.org/beyond-prolog/mcp-workflows/","summary":"\u003cp\u003eProlog is a pure inference engine. It answers queries; it does not \u003cem\u003edo\u003c/em\u003e things. There is no\nstandard way to call an external tool, and nothing reactive — a fact changing can\u0026rsquo;t fire an\naction, because the language has no notion of \u0026ldquo;an action\u0026rdquo; or of \u0026ldquo;changing.\u0026rdquo; In practice you bolt\non non-standard extensions and hand-roll all the orchestration yourself.\u003c/p\u003e\n\u003cp\u003eTln makes tools and reactivity first-class, through the \u003cstrong\u003eModel Context Protocol (MCP)\u003c/strong\u003e. This is\nthe capability that turns a ruleset into a running agent — and it\u0026rsquo;s what powers\n\u003ca href=\"/opentalon/\"\u003eOpenTalon\u003c/a\u003e in production.\u003c/p\u003e","title":"MCP tools \u0026 workflows"},{"content":"Expert systems hit a wall the moment a decision needs a learned judgement — \u0026ldquo;is this reading anomalous?\u0026rdquo;, \u0026ldquo;what\u0026rsquo;s this incident\u0026rsquo;s likely cause?\u0026rdquo;, \u0026ldquo;when will we run out?\u0026rdquo;. In Prolog you leave the language: export the data, run a Python/R pipeline, glue the answer back. Tln builds the common cases in as blocks, each returning an explanation alongside its value — so an ML result is as auditable as any rule.\nThe primitives Eleven statistical/ML primitives ship in the runtime, each traceable:\nBlock / use Primitive detect … is anomaly z-score outlier · Grubbs\u0026rsquo; test threshold (adaptive) learned threshold (percentile/avg from your data) detect … correlates_with Pearson correlation forecast weighted moving average · exponential smoothing cluster DBSCAN find similar cosine similarity find related Personalized PageRank classify k-nearest-neighbours predict CART decision tree Predict — decision tree Train on retired machines, predict the outcome for in-service ones. Model and inference are one block; confidence gates the result:\npredict \u0026#34;Failure risk\u0026#34; { for records where type == \u0026#34;machine\u0026#34; and status == \u0026#34;in_service\u0026#34; features [attr \u0026#34;operating_hours\u0026#34;, attr \u0026#34;repair_count\u0026#34;] trained_on records where type == \u0026#34;machine\u0026#34; and status == \u0026#34;retired\u0026#34; label_attr \u0026#34;outcome\u0026#34; confidence \u0026gt;= 0.9 label \u0026#34;predicted outcome: {class}\u0026#34; } Classify — k-NN classify \u0026#34;Failure mode\u0026#34; { for records where type == \u0026#34;incident\u0026#34; and status == \u0026#34;open\u0026#34; features [attr \u0026#34;vibration\u0026#34;, attr \u0026#34;temp\u0026#34;] trained_on records where type == \u0026#34;incident\u0026#34; and status == \u0026#34;resolved\u0026#34; label_attr \u0026#34;root_cause\u0026#34; confidence \u0026gt;= 0.8 label \u0026#34;likely cause: {class}\u0026#34; } Forecast — time series forecast \u0026#34;Parts stock-out\u0026#34; { for records where type == \u0026#34;stock_item\u0026#34; and status == \u0026#34;active\u0026#34; series attr \u0026#34;current_stock\u0026#34; over last 90 days label \u0026#34;{item.name}: stock-out in ~{days_until} days\u0026#34; } Models can also be declared once and shared: a model block carries fitted examples plus provenance (computed_from, valid_until), exported from a module and pulled in with using model \u0026quot;fleet.ml.failure_risk\u0026quot;. Same determinism, same explainability — just learned from data instead of hand-written.\n","permalink":"https://tln-lang.org/beyond-prolog/ml/","summary":"\u003cp\u003eExpert systems hit a wall the moment a decision needs a \u003cem\u003elearned\u003c/em\u003e judgement — \u0026ldquo;is this reading\nanomalous?\u0026rdquo;, \u0026ldquo;what\u0026rsquo;s this incident\u0026rsquo;s likely cause?\u0026rdquo;, \u0026ldquo;when will we run out?\u0026rdquo;. In Prolog you leave\nthe language: export the data, run a Python/R pipeline, glue the answer back. Tln builds the\ncommon cases in as blocks, each returning an \u003cstrong\u003eexplanation\u003c/strong\u003e alongside its value — so an ML result\nis as auditable as any rule.\u003c/p\u003e","title":"Built-in ML"},{"content":"This is the comparison closest to Prolog\u0026rsquo;s heart: derived predicates and recursive rules. Tln keeps the Datalog surface Prolog programmers already know — and fixes the one place ISO Prolog\u0026rsquo;s negation quietly breaks.\nDerived predicates A derive block names a boolean predicate over a record; any other block references it as pred(v), exactly like an asserted fact. It\u0026rsquo;s the same idea as a Prolog rule head — but the planner inlines it, and the blocks that use it stay declarative (flag / label instead of a forall/format loop). Both programs below are real: the Prolog is ISO-standard and SWI-checked, the Tln is examples/vehicle_recall.tln.\nISO Prolog (1995)% \u0026#34;overdue\u0026#34; is a derived predicate — a rule head. overdue(E, Id) :- record(E, Id, vehicle, _, _, _), attr(E, Id, km, Km), attr(E, Id, last_service_km, Last), Km \u0026gt; Last + 20000. recall_candidate(E, Id) :- overdue(E, Id), attr(E, Id, model, Model), member(Model, [\u0026#39;Transit\u0026#39;, \u0026#39;Sprinter\u0026#39;]). % Chaining is natural; flagging + labelling is manual. report_recalls(E) :- forall(recall_candidate(E, Id), ( attr(E, Id, name, Name), attr(E, Id, model, Model), format(\u0026#34;~w: recall candidate (model ~w)~n\u0026#34;, [Name, Model]) )). Tlnderive overdue(v) { for records where type == \u0026#34;vehicle\u0026#34; and attr \u0026#34;km\u0026#34; \u0026gt; attr \u0026#34;last_service_km\u0026#34; + 20000 } detect \u0026#34;Recall candidates\u0026#34; { for records where overdue(v) and attr \u0026#34;model\u0026#34; in [\u0026#34;Transit\u0026#34;, \u0026#34;Sprinter\u0026#34;] flag matching items label \u0026#34;{item.name}: recall candidate ({attr.km} km, model {attr.model})\u0026#34; } recommend \u0026#34;Book recall service\u0026#34; { when detect \u0026#34;Recall candidates\u0026#34; matches suggest \u0026#34;book {item.name} in for the recall service\u0026#34; } Same deduction, but the Tln chain derive → detect → recommend runs end-to-end with no host glue, and tln explain will name the derived predicate in its trace:\nWHY • satisfies derived overdue(v) Negation through recursion Here\u0026rsquo;s where the standard bites. The canonical logic-programming example is the game of positions — a position is winning if some move leads to a non-winning position:\nwin(X) :- move(X, Y), \\+ win(Y). On a graph like a → b with b terminal, this is fine: win(b) is false, win(a) is true. But add a draw — a 2-cycle a ⇄ b — and ISO Prolog\u0026rsquo;s negation-as-failure (SLDNF) has no sound answer: the goal recurses through \\+ win(Y) into itself and loops.\nTln\u0026rsquo;s recursive resolver takes the negative literal and computes the rule set\u0026rsquo;s well-founded model — a unique three-valued interpretation where every atom is true, false, or undefined:\ngraph Tln result a → b, b terminal win(a) true, win(b) false a ⇄ b (a draw) win(a), win(b) both undefined The draw is exactly where well-founded semantics earns its keep: instead of looping or guessing, Tln says undefined and means it. (Recursive/negated rules currently live at the engine level — see docs/well-founded.md; a .tln surface syntax rides with self-hosting.)\nBounded recursion with guards Real recursive Prolog leans on arithmetic — but usually as guards, not term construction: \u0026ldquo;reachable within N hops\u0026rdquo;, \u0026ldquo;follow edges while the running weight stays under a cap\u0026rdquo;, \u0026ldquo;walk only nodes whose name starts with…\u0026rdquo;. Tln\u0026rsquo;s recursive resolver evaluates comparison (\u0026lt; \u0026lt;= \u0026gt; \u0026gt;= !=), string (starts_with / contains / …), and membership (in / not_in) predicates as guards inside a recursive rule body — on both the top-down and well-founded resolvers.\nA guard only filters already-bound values; it binds no fresh variable and invents nothing outside the facts, so the fixpoint still terminates. That moves bounded reachability, threshold/weight walks, and string-filtered recursion from engine-only to native, terminating Tln rules.\nWhat stays on tln-prolog: value-inventing arithmetic — e.g. N1 is N - 1 fed back into the recursion — which builds new values and would break the finite-model guarantee.\nTakeaway For everyday deduction Tln stays deliberately close to Prolog — you\u0026rsquo;re writing rule heads and bodies. The differences are downstream: derivations inline into declarative detect/recommend blocks, results are testable and explainable, and recursion-with-negation gets a defined answer instead of an infinite loop.\n","permalink":"https://tln-lang.org/comparisons/reasoning-recursion/","summary":"\u003cp\u003eThis is the comparison closest to Prolog\u0026rsquo;s heart: \u003cstrong\u003ederived predicates\u003c/strong\u003e and \u003cstrong\u003erecursive rules\u003c/strong\u003e.\nTln keeps the Datalog surface Prolog programmers already know — and fixes the one place ISO\nProlog\u0026rsquo;s negation quietly breaks.\u003c/p\u003e\n\u003ch2 id=\"derived-predicates\"\u003eDerived predicates\u003c/h2\u003e\n\u003cp\u003eA \u003ccode\u003ederive\u003c/code\u003e block names a boolean predicate over a record; any other block references it as\n\u003ccode\u003epred(v)\u003c/code\u003e, exactly like an asserted fact. It\u0026rsquo;s the same idea as a Prolog rule head — but the\nplanner \u003cem\u003einlines\u003c/em\u003e it, and the blocks that use it stay declarative (\u003ccode\u003eflag\u003c/code\u003e / \u003ccode\u003elabel\u003c/code\u003e instead of a\n\u003ccode\u003eforall\u003c/code\u003e/\u003ccode\u003eformat\u003c/code\u003e loop). Both programs below are real: the Prolog is ISO-standard and\nSWI-checked, the Tln is \u003ca href=\"https://github.com/opentalon/tln-language/blob/master/examples/vehicle_recall.tln\"\u003e\u003ccode\u003eexamples/vehicle_recall.tln\u003c/code\u003e\u003c/a\u003e.\u003c/p\u003e","title":"Derived predicates \u0026 recursion"},{"content":"Facts are loaded from external systems (see the mental model); .tln files contain blocks that reason over them.\nValues 42 3.14 // numbers \u0026#34;outpatient\u0026#34; // strings — always double-quoted true false // booleans 7 days 30 days 12 months 1 year // durations [\u0026#34;health\u0026#34;, \u0026#34;cancellation\u0026#34;] [100, 200] // lists Operators \u0026gt; \u0026lt; \u0026gt;= \u0026lt;= == != ~= // comparison + - * / % // arithmetic and or not // logical in [ … ] not in [ … ] // membership contains starts_with ends_with older_than newer_than // string / temporal Selectors A selector picks which records a block runs over:\nfor records where type == \u0026#34;product\u0026#34; and category == \u0026#34;van\u0026#34; and attr \u0026#34;price\u0026#34; \u0026gt; 100 and status == \u0026#34;active\u0026#34; and is \u0026#34;high_value\u0026#34; // reference a define Core blocks define — reusable conditions define \u0026#34;high_value\u0026#34; { attr \u0026#34;amount_chf\u0026#34; \u0026gt; 10000 } rule — enforce a constraint (allow / block) rule \u0026#34;Reject blacklisted provider\u0026#34; { for records where type == \u0026#34;claim\u0026#34; and attr \u0026#34;provider_status\u0026#34; == \u0026#34;blacklisted\u0026#34; block \u0026#34;approve_claim\u0026#34; reason \u0026#34;Provider {attr.provider_id} is on the fraud blacklist\u0026#34; } Swap block for allow to auto-approve. Higher priority wins conflicts; a strict rule is non-negotiable and an overrides \u0026quot;Other rule\u0026quot; rule defeats a named one.\ndetect — find patterns and flag them detect \u0026#34;Over the per-visit cap\u0026#34; { for records where type == \u0026#34;claim\u0026#34; and attr \u0026#34;amount_chf\u0026#34; \u0026gt; attr \u0026#34;per_visit_cap\u0026#34; flag matching items label \u0026#34;Claim {item.id}: {attr.amount_chf} CHF over cap\u0026#34; } recommend — suggest the next step recommend \u0026#34;Schedule reviewer\u0026#34; { when detect \u0026#34;Over the per-visit cap\u0026#34; matches suggest \u0026#34;Route claim {item.id} to a senior adjuster\u0026#34; } combine — optimal combinations combine \u0026#34;Reorder picks\u0026#34; { for records where type == \u0026#34;stock_item\u0026#34; and status == \u0026#34;active\u0026#34; select 3 from records minimize total(attr \u0026#34;reorder_cost\u0026#34;) subject_to total(attr \u0026#34;reorder_cost\u0026#34;) \u0026lt;= 5000 return id, reorder_cost } combine runs real multi-objective optimization (Pareto / genetic / ant-colony / ILP backends); add more minimize / maximize objectives and subject_to constraints as needed.\nTemplates label, reason, and suggest strings interpolate {…}:\n{attr.\u0026lt;name\u0026gt;} a record\u0026#39;s attribute {item.name} the matched item {count} number of matches {item.id} the matched id {total(attr.\u0026lt;name\u0026gt;)} sum over matches {avg(attr.\u0026lt;name\u0026gt;)} {days_until(\u0026lt;date\u0026gt;)} days until a date {days_since(\u0026lt;date\u0026gt;)} Priorities CRITICAL immediate action HIGH within days MEDIUM within weeks LOW informational Metaprogramming — compile-time macros Tln has compile-time macros built into core — defmacro / quote / unquote, Elixir-style — that generate rules before validation, so boilerplate disappears while the runtime stays pure and deterministic. See Metaprogramming for the full worked example and a side-by-side with Prolog\u0026rsquo;s term_expansion.\nPriorities and beyond Beyond these core blocks, Tln adds ML (predict, forecast, classify, cluster, find), MCP orchestration (workflow, on, collect, enrich), reactive on change blocks, and integrity constraints — all in Beyond Prolog.\n","permalink":"https://tln-lang.org/docs/reference/","summary":"\u003cp\u003eFacts are loaded from external systems (see \u003ca href=\"/docs/getting-started/#the-mental-model\"\u003ethe mental model\u003c/a\u003e);\n\u003ccode\u003e.tln\u003c/code\u003e files contain \u003cstrong\u003eblocks\u003c/strong\u003e that reason over them.\u003c/p\u003e\n\u003ch2 id=\"values\"\u003eValues\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode class=\"language-tln\" data-lang=\"tln\"\u003e42            3.14                 // numbers\n\u0026#34;outpatient\u0026#34;                       // strings — always double-quoted\ntrue          false                // booleans\n7 days   30 days   12 months   1 year   // durations\n[\u0026#34;health\u0026#34;, \u0026#34;cancellation\u0026#34;]   [100, 200]  // lists\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"operators\"\u003eOperators\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u0026gt;  \u0026lt;  \u0026gt;=  \u0026lt;=  ==  !=  ~=        // comparison\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e+  -  *  /  %                   // arithmetic\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eand  or  not                   // logical\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ein [ … ]      not in [ … ]      // membership\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003econtains   starts_with   ends_with   older_than   newer_than   // string / temporal\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"selectors\"\u003eSelectors\u003c/h2\u003e\n\u003cp\u003eA selector picks which records a block runs over:\u003c/p\u003e","title":"Language Reference"},{"content":"Vehicle service tracking from examples/fleet_maintenance.tln: flag active vehicles overdue for service, then forecast a parts stock-out.\nService overdue Two named conditions compose into a detection. In Prolog these are rule heads and a manual report predicate; in Tln they\u0026rsquo;re defines referenced with is, feeding a declarative detect.\nISO Prolog (1995)active_vehicle(E, Id) :- record(E, Id, item, \u0026#39;Vehicles\u0026#39;, active, _). overdue_km(E, Id) :- attr(E, Id, km, Km), attr(E, Id, last_service_km, Last), Km \u0026gt; Last. service_overdue(E, Id) :- active_vehicle(E, Id), overdue_km(E, Id). report_overdue(E) :- forall(service_overdue(E, Id), ( attr(E, Id, name, Name), attr(E, Id, km, Km), attr(E, Id, last_service_km, Last), format(\u0026#34;~w: ~w km since last service at ~w km~n\u0026#34;, [Name, Km, Last]) )). Tlndefine \u0026#34;active_vehicle\u0026#34; { type == \u0026#34;item\u0026#34; and status == \u0026#34;active\u0026#34; and category == \u0026#34;Vehicles\u0026#34; } define \u0026#34;overdue_km\u0026#34; { attr \u0026#34;km\u0026#34; \u0026gt; attr \u0026#34;last_service_km\u0026#34; } detect \u0026#34;Service overdue\u0026#34; { for records where is \u0026#34;active_vehicle\u0026#34; and is \u0026#34;overdue_km\u0026#34; flag matching items label \u0026#34;{item.name}: {attr.km} km since last service at {attr.last_service_km} km\u0026#34; } A forecast Prolog can\u0026rsquo;t express The same file then predicts when a part will run out — a time-series forecast over the last 90 days of stock levels:\nforecast \u0026#34;Parts stock-out\u0026#34; { for records where type == \u0026#34;stock_item\u0026#34; and status == \u0026#34;active\u0026#34; series attr \u0026#34;current_stock\u0026#34; over last 90 days label \u0026#34;{item.name}: stock-out in ~{days_until} days\u0026#34; } There\u0026rsquo;s no left pane here on purpose. ISO Prolog has no notion of a time series or exponential smoothing — you\u0026rsquo;d leave the language entirely, push the data into Python or R, and glue the result back. Tln ships forecasting (and anomaly detection, classification, clustering, similarity) as first-class blocks with explainable output — see Beyond Prolog → ML.\n","permalink":"https://tln-lang.org/comparisons/fleet-maintenance/","summary":"\u003cp\u003eVehicle service tracking from \u003ca href=\"https://github.com/opentalon/tln-language/blob/master/examples/fleet_maintenance.tln\"\u003e\u003ccode\u003eexamples/fleet_maintenance.tln\u003c/code\u003e\u003c/a\u003e:\nflag active vehicles overdue for service, then forecast a parts stock-out.\u003c/p\u003e\n\u003ch2 id=\"service-overdue\"\u003eService overdue\u003c/h2\u003e\n\u003cp\u003eTwo named conditions compose into a detection. In Prolog these are rule heads and a manual report\npredicate; in Tln they\u0026rsquo;re \u003ccode\u003edefine\u003c/code\u003es referenced with \u003ccode\u003eis\u003c/code\u003e, feeding a declarative \u003ccode\u003edetect\u003c/code\u003e.\u003c/p\u003e\n\u003cdiv class=\"compare\"\u003e\n  \n\u003cdiv class=\"compare-pane compare-pane--prolog\"\u003e\u003cdiv class=\"compare-pane__title\"\u003eISO Prolog (1995)\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-prolog\" data-lang=\"prolog\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003eactive_vehicle\u003c/span\u003e(E, Id) :-\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003erecord\u003c/span\u003e(E, Id, \u003cspan style=\"color:#e6db74\"\u003eitem\u003c/span\u003e, \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;Vehicles\u0026#39;\u003c/span\u003e, \u003cspan style=\"color:#e6db74\"\u003eactive\u003c/span\u003e, \u003cspan style=\"color:#66d9ef\"\u003e_\u003c/span\u003e).\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003eoverdue_km\u003c/span\u003e(E, Id) :-\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eattr\u003c/span\u003e(E, Id, \u003cspan style=\"color:#e6db74\"\u003ekm\u003c/span\u003e, Km),\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eattr\u003c/span\u003e(E, Id, \u003cspan style=\"color:#e6db74\"\u003elast_service_km\u003c/span\u003e, Last),\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    Km \u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e Last.\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003eservice_overdue\u003c/span\u003e(E, Id) :-\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eactive_vehicle\u003c/span\u003e(E, Id),\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eoverdue_km\u003c/span\u003e(E, Id).\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003ereport_overdue\u003c/span\u003e(E) :-\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eforall\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003eservice_overdue\u003c/span\u003e(E, Id),\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e           ( \u003cspan style=\"color:#a6e22e\"\u003eattr\u003c/span\u003e(E, Id, \u003cspan style=\"color:#e6db74\"\u003ename\u003c/span\u003e, Name),\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e             \u003cspan style=\"color:#a6e22e\"\u003eattr\u003c/span\u003e(E, Id, \u003cspan style=\"color:#e6db74\"\u003ekm\u003c/span\u003e, Km),\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e             \u003cspan style=\"color:#a6e22e\"\u003eattr\u003c/span\u003e(E, Id, \u003cspan style=\"color:#e6db74\"\u003elast_service_km\u003c/span\u003e, Last),\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e             \u003cspan style=\"color:#a6e22e\"\u003eformat\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;~w: ~w km since last service at ~w km~n\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    [Name, Km, Last]) )).\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\n\u003cdiv class=\"compare-pane compare-pane--tln\"\u003e\u003cdiv class=\"compare-pane__title\"\u003eTln\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-tln\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edefine \u0026#34;active_vehicle\u0026#34; {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  type == \u0026#34;item\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  and status == \u0026#34;active\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  and category == \u0026#34;Vehicles\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edefine \u0026#34;overdue_km\u0026#34; {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  attr \u0026#34;km\u0026#34; \u0026gt; attr \u0026#34;last_service_km\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edetect \u0026#34;Service overdue\u0026#34; {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  for records where is \u0026#34;active_vehicle\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    and is \u0026#34;overdue_km\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  flag matching items\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  label \u0026#34;{item.name}: {attr.km} km since last service at {attr.last_service_km} km\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\n\n\u003c/div\u003e\n\n\u003ch2 id=\"a-forecast-prolog-cant-express\"\u003eA forecast Prolog can\u0026rsquo;t express\u003c/h2\u003e\n\u003cp\u003eThe same file then predicts \u003cem\u003ewhen\u003c/em\u003e a part will run out — a time-series forecast over the last 90\ndays of stock levels:\u003c/p\u003e","title":"Fleet maintenance"},{"content":"A ruleset that decides claims, or blocks a deployment, or reorders stock, needs to be tested — not eyeballed. ISO Prolog has no standard test framework (SWI ships the non-standard plunit); Tln has one built in, and it needs no database.\ngiven / when / expect A .tln.test file seeds facts in a given block, runs a named block with when, and asserts on the result:\ntest \u0026#34;greets the world\u0026#34; { given { record 1 type \u0026#34;greeting\u0026#34; attr 1 \u0026#34;name\u0026#34; \u0026#34;World\u0026#34; } when detect \u0026#34;Hello, World\u0026#34; expect { flagged 1 label contains \u0026#34;Hello, World!\u0026#34; count == 1 } } The given block is the fact schema you\u0026rsquo;d otherwise load from an external system — record and attr triples — so tests are hermetic and fast (the runner materializes them in memory; no FactStore required).\nAssertions flagged \u0026lt;id\u0026gt; not flagged \u0026lt;id\u0026gt; label contains \u0026#34;\u0026lt;text\u0026gt;\u0026#34; priority == LOW|MEDIUM|HIGH|CRITICAL count == \u0026lt;n\u0026gt; A realistic test pins down exactly which records should and shouldn\u0026rsquo;t fire — here, an overdue- service detection separating active-overdue vehicles from up-to-date ones, inactive ones, and non-vehicles:\ntest \u0026#34;Overdue service flags only overdue vehicles\u0026#34; { given { record 501 type \u0026#34;item\u0026#34; category \u0026#34;Vehicles\u0026#34; status \u0026#34;active\u0026#34; attr 501 \u0026#34;km\u0026#34; 45000 attr 501 \u0026#34;last_service_km\u0026#34; 20000 attr 501 \u0026#34;name\u0026#34; \u0026#34;Truck A\u0026#34; record 502 type \u0026#34;item\u0026#34; category \u0026#34;Vehicles\u0026#34; status \u0026#34;active\u0026#34; attr 502 \u0026#34;km\u0026#34; 25000 attr 502 \u0026#34;last_service_km\u0026#34; 25000 attr 502 \u0026#34;name\u0026#34; \u0026#34;Van B\u0026#34; } when detect \u0026#34;Service overdue\u0026#34; expect { flagged 501 not flagged 502 label contains \u0026#34;Truck A\u0026#34; } } Running tln test rules.tln rules.tln.test # ==\u0026gt; rules.tln.test: 1 test(s) # # 1 passed, 0 failed tln test rules.tln rules.tln.test -run \u0026#34;Overdue\u0026#34; -v # filter + verbose tln test rules.tln rules.tln.test --junit out.xml # CI-friendly report Because the engine is deterministic, a passing test stays passing for the same facts — the property that makes an expert system safe to put in front of real decisions.\n","permalink":"https://tln-lang.org/beyond-prolog/testing/","summary":"\u003cp\u003eA ruleset that decides claims, or blocks a deployment, or reorders stock, needs to be \u003cem\u003etested\u003c/em\u003e —\nnot eyeballed. ISO Prolog has no standard test framework (SWI ships the non-standard \u003ccode\u003eplunit\u003c/code\u003e);\nTln has one built in, and it needs no database.\u003c/p\u003e\n\u003ch2 id=\"given--when--expect\"\u003e\u003ccode\u003egiven\u003c/code\u003e / \u003ccode\u003ewhen\u003c/code\u003e / \u003ccode\u003eexpect\u003c/code\u003e\u003c/h2\u003e\n\u003cp\u003eA \u003ccode\u003e.tln.test\u003c/code\u003e file seeds facts in a \u003ccode\u003egiven\u003c/code\u003e block, runs a named block with \u003ccode\u003ewhen\u003c/code\u003e, and asserts on\nthe result:\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode class=\"language-tln\" data-lang=\"tln\"\u003etest \u0026#34;greets the world\u0026#34; {\n  given {\n    record 1 type \u0026#34;greeting\u0026#34;\n    attr 1 \u0026#34;name\u0026#34; \u0026#34;World\u0026#34;\n  }\n  when detect \u0026#34;Hello, World\u0026#34;\n  expect {\n    flagged 1\n    label contains \u0026#34;Hello, World!\u0026#34;\n    count == 1\n  }\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eThe \u003ccode\u003egiven\u003c/code\u003e block \u003cem\u003eis\u003c/em\u003e the fact schema you\u0026rsquo;d otherwise load from an external system — \u003ccode\u003erecord\u003c/code\u003e and\n\u003ccode\u003eattr\u003c/code\u003e triples — so tests are hermetic and fast (the runner materializes them in memory; no\n\u003ccode\u003eFactStore\u003c/code\u003e required).\u003c/p\u003e","title":"Testing"},{"content":"Tln does metaprogramming the Elixir way: macros are code that writes code, and they run at compile time. A defmacro expands into ordinary blocks before validation and planning — so the validator, planner, and runtime never see a macro, and the engine stays exactly as deterministic and terminating as always. The one place unbounded computation is allowed is expansion itself, bounded by a step budget (a compile error, never a runtime hang). That\u0026rsquo;s also why macros live in core, not a plugin: only core owns the grammar and the compile phases.\nProlog can metaprogram too — it\u0026rsquo;s homoiconic — via the classic term_expansion/2 hook (and its runtime cousins =.., call, assert). So this is less \u0026ldquo;Prolog can\u0026rsquo;t\u0026rdquo; than \u0026ldquo;Tln does it differently\u0026rdquo;: quote turns a block into an AST value, unquote splices a value in, and defmacro is a compile-time function from arguments to AST. Here one macro kills the boilerplate across near-identical detect rules:\nISO Prolog (1995)% Compile-time metaprogramming via term_expansion/2: % each over_threshold/3 fact is rewritten, as it is read, % into a high/1 rule. term_expansion(over_threshold(Name, Metric, Limit), ( high(Item) :- record(_, Item, item, _, _, _), attr(_, Item, Metric, V), V \u0026gt; Limit )). over_threshold(temperature, temp_c, 80). over_threshold(pressure, psi, 200). Tlndefmacro over_threshold(name, metric, limit, prio) { quote { detect \u0026#34;High {unquote(name)}\u0026#34; { for records where type == \u0026#34;item\u0026#34; and attr unquote(metric) \u0026gt; unquote(limit) flag matching items label \u0026#34;{item.name}: high {unquote(name)}\u0026#34; priority unquote(prio) } } } over_threshold(\u0026#34;temperature\u0026#34;, \u0026#34;temp_c\u0026#34;, 80, HIGH) over_threshold(\u0026#34;pressure\u0026#34;, \u0026#34;psi\u0026#34;, 200, MEDIUM) The difference is what they rewrite and when. Prolog\u0026rsquo;s term_expansion (and =.. / call) operate on Prolog terms — function symbols that flat-EAV Tln core doesn\u0026rsquo;t have, so that runtime \u0026ldquo;code-as-data\u0026rdquo; belongs to the tln-prolog engine. Tln\u0026rsquo;s macros expand to AST blocks at compile time, leaving the runtime a pure, deterministic Datalog. The macro above expands into exactly two ordinary detect blocks — all the validator, planner, and runtime ever see:\ndetect \u0026#34;High temperature\u0026#34; { for records where type == \u0026#34;item\u0026#34; and attr \u0026#34;temp_c\u0026#34; \u0026gt; 80 flag matching items label \u0026#34;{item.name}: high temperature\u0026#34; priority HIGH } detect \u0026#34;High pressure\u0026#34; { for records where type == \u0026#34;item\u0026#34; and attr \u0026#34;psi\u0026#34; \u0026gt; 200 flag matching items label \u0026#34;{item.name}: high pressure\u0026#34; priority MEDIUM } ","permalink":"https://tln-lang.org/beyond-prolog/metaprogramming/","summary":"\u003cp\u003eTln does metaprogramming the \u003cstrong\u003eElixir way\u003c/strong\u003e: macros are code that writes code, and they run at\n\u003cstrong\u003ecompile time\u003c/strong\u003e. A \u003ccode\u003edefmacro\u003c/code\u003e expands into ordinary blocks \u003cem\u003ebefore\u003c/em\u003e validation and planning — so\nthe validator, planner, and runtime never see a macro, and the engine stays exactly as\ndeterministic and terminating as always. The one place unbounded computation is allowed is\nexpansion itself, bounded by a step budget (a compile error, never a runtime hang). That\u0026rsquo;s also\nwhy macros live \u003cstrong\u003ein core, not a plugin\u003c/strong\u003e: only core owns the grammar and the compile phases.\u003c/p\u003e","title":"Metaprogramming"},{"content":"Tln\u0026rsquo;s language core is a pure language + planner: it decides which facts to read, which tool calls to fire, and which rules to run — and returns them as data. It performs no IO and no non-deterministic search itself. Every edge — storage, tools, solvers, channels — is a plugin injected by the host. That\u0026rsquo;s what keeps the core deterministic and testable, and the system extensible.\nTools — the tool verb A block calls a tool with the plugin-neutral tool verb — tool \u0026quot;server\u0026quot; \u0026quot;name\u0026quot; { … }. The server name routes to a host-injected ToolResolver; nothing about the transport is baked into the rule:\nworkflow \u0026#34;Notify low stock\u0026#34; { step \u0026#34;reorder\u0026#34; { tool \u0026#34;inventory\u0026#34; \u0026#34;create-refill-order\u0026#34; { item_id item.id quantity 5 } } step \u0026#34;announce\u0026#34; { tool \u0026#34;slack\u0026#34; \u0026#34;post-message\u0026#34; { channel \u0026#34;#ops\u0026#34; text \u0026#34;reordered {item.id}\u0026#34; } } } tln-mcp is the ready-made resolver that routes such server names over the Model Context Protocol (JSON-RPC); a mock, a direct HTTP client, or an internal bus can stand in without touching a rule. A connector block binds a server to a plugin in source, with env-resolved credentials — so a program runs with no Go host:\nconnector \u0026#34;inventory\u0026#34; via mcp { endpoint env \u0026#34;INVENTORY_ENDPOINT\u0026#34; bearer env \u0026#34;INVENTORY_TOKEN\u0026#34; } collect / enrich / remediate dispatch through the same resolver. See MCP \u0026amp; workflows for more.\nI/O — io-tln Not every tool call goes to a remote server. Tln core is effect-free: it decides which effects fire and hands them back as data; performing them is a plugin\u0026rsquo;s job. io-tln is the plugin for the most basic effect — I/O — injected exactly like tln-mcp. You call the io server like any other tool:\ndetect \u0026#34;Overdue for service\u0026#34; { for records where type == \u0026#34;vehicle\u0026#34; and attr \u0026#34;km\u0026#34; \u0026gt; attr \u0026#34;last_service_km\u0026#34; + 20000 flag matching items remediate { tool \u0026#34;io\u0026#34; \u0026#34;writeln\u0026#34; { text \u0026#34;overdue: {item.id} at {attr.km} km\u0026#34; } if attr \u0026#34;priority\u0026#34; == \u0026#34;CRITICAL\u0026#34; { tool \u0026#34;io\u0026#34; \u0026#34;eprintln\u0026#34; { text \u0026#34;CRITICAL: {item.id}\u0026#34; } } } } The tools are write/writeln (and print/println), write_err/eprintln, format (printf-style), and read/read_line (which binds the line back as a value). A connector picks the destination — the name you call is the connector:\nconnector \u0026#34;io\u0026#34; via io { } # stdout (default) connector \u0026#34;errs\u0026#34; via io { stream stderr } # stderr connector \u0026#34;audit\u0026#34; via io { path \u0026#34;/var/log/tln/audit.log\u0026#34; } # append to a file tool \u0026#34;audit\u0026#34; \u0026#34;writeln\u0026#34; { text \u0026#34;overdue: {item.id}\u0026#34; } # → the file io needs no credentials (env \u0026quot;…\u0026quot; is an mcp concern); the runtime opens the file/stream and hands the plugin the writer/reader — no paths in the rule.\nStorage — tln-db The other SPI is the FactStore. tln-db is the Go-native fact store behind it — embed it as a library or run it as a gRPC/HTTP sidecar. The same interface accepts other backends (in-memory for tests, Datalevin, …). Details on the DB page.\nSolver — tln-asp Tln core is deterministic: its well-founded resolver yields a single three-valued model (true / false / undefined). But some problems — planning, configuration, combinatorial search — have zero or many solutions. That\u0026rsquo;s Answer Set Programming (stable-model semantics), kept out of core by design and owned by the tln-asp plugin: a pure-Go stable-model solver.\nThe classic example: a position is winning if some move leads to a position that is not winning — win defined through its own negation.\nISO Prolog (1995)% Recursion through negation. Under SLDNF this loops on a % cycle (a draw) — the rule set has no single model. win(X) :- move(X, Y), \\+ win(Y). move(a, b). Tln \u0026#43; tln-asp// Written the same way. Tln core rejects negation-through- // recursion as \u0026#34;not stratifiable\u0026#34;, so the tln-asp plugin // solves it and enumerates the answer sets. derive win(x) { for records where move(x, y) and not win(y) } detect \u0026#34;Winning positions\u0026#34; { for records where type == \u0026#34;position\u0026#34; and win(pos) flag matching items label \u0026#34;{item.name}: winning\u0026#34; } On a cycle (a draw) the rule has multiple answer sets — undefined for core\u0026rsquo;s single well-founded model, but exactly the ASP case. The host builds the rule set from the public pkg/factstore types and hands it to tln-asp, which enumerates the answer sets; each feeds back into any FactStore.\nThe pattern ┌────────────────────────────┐ facts ─────► │ Tln core: parse → plan → │ ─────► tool calls (as data) │ evaluate (deterministic) │ └──────┬─────────┬─────────┬──┘ │ SPI │ SPI │ SPI FactStore ToolResolver Solver tln-db tln-mcp · io-tln tln-asp Two ways to run — standalone or hosted Because the core is transport-free, the same .tln program runs in two modes, resolved by a fixed precedence: host binding → connector block → built-in io → error.\nStandalone. The program declares its own connector blocks — with env for endpoints and credentials — so it runs with no Go host. tln run wires the plugins the source names, and the built-in io server needs no declaration:\nconnector \u0026#34;inventory\u0026#34; via mcp { endpoint env \u0026#34;INVENTORY_ENDPOINT\u0026#34; bearer env \u0026#34;INVENTORY_TOKEN\u0026#34; } detect \u0026#34;Overdue\u0026#34; { for records where type == \u0026#34;vehicle\u0026#34; and attr \u0026#34;km\u0026#34; \u0026gt; attr \u0026#34;service_due_km\u0026#34; flag matching items remediate { tool \u0026#34;inventory\u0026#34; \u0026#34;create-refill-order\u0026#34; { item_id item.id quantity 5 } tool \u0026#34;io\u0026#34; \u0026#34;writeln\u0026#34; { text \u0026#34;reordered {item.id}\u0026#34; } } } Hosted (e.g. OpenTalon). A Go host binds the plugins itself (tln.WithToolResolver(…)), and that binding wins — the same rules run unchanged, with the host owning endpoints, credentials, and I/O. For LLM-authored source the host sandboxes it: env is cut and io restricted, so a generated program can\u0026rsquo;t reach secrets or the filesystem.\nTwo guardrails make this safe: env is connector-scoped (it parses only inside a connector\u0026rsquo;s config — never in a label, a stored fact, or a tool argument, so a credential can\u0026rsquo;t leak into data), and connector/env are author-only (a metaprogramming macro may emit tool calls but never a connector).\nProlog runtime — tln-prolog The fourth plugin is aimed squarely at the migration story: tln-prolog is a pure-Go Prolog engine — so a Prolog program can run in the Tln world with no Prolog installed (no SWI, no GNU).\nPorting is the point: Prolog is the source, Tln is the target. The relational subset of a .pl file lowers to native Tln rules on the core engine — including recursive rules whose arithmetic is just a guard (comparisons, string tests, membership: bounded reachability, threshold/weight walks). But core is flat-EAV / Datalog with no function symbols, so Prolog\u0026rsquo;s compound terms and lists, cut-dependent control, assert/retract, and value-inventing arithmetic (N1 is N-1 fed back into recursion) can\u0026rsquo;t become core rules. Those parts run on tln-prolog instead — same ecosystem, still no external Prolog:\n% Lists + compound terms — no flat-EAV / Datalog equivalent, % so this keeps running on the tln-prolog engine, unchanged. conc([], L, L). conc([H|T], L, [H|R]) :- conc(T, L, R). It carries what core deliberately lacks: structured terms (Var · Atom · Int · Compound), unification with a sound occurs-check, a depth-bounded SLD machine (backtracking, fresh-clause renaming), and an ISO-subset .pl reader that never drops anything silently — unsupported constructs come back as typed diagnostics. Answers project to []factstore.Fact, so results flow into any Tln FactStore.\nSo the Prolog → Tln comparisons aren\u0026rsquo;t only \u0026ldquo;rewrite by hand\u0026rdquo;: existing Prolog can be ported — the relational parts become Tln rules, the rest keeps running on tln-prolog.\n","permalink":"https://tln-lang.org/plugins/","summary":"\u003cp\u003eTln\u0026rsquo;s language core is a \u003cstrong\u003epure language + planner\u003c/strong\u003e: it decides \u003cem\u003ewhich\u003c/em\u003e facts to read, \u003cem\u003ewhich\u003c/em\u003e\ntool calls to fire, and \u003cem\u003ewhich\u003c/em\u003e rules to run — and returns them as \u003cstrong\u003edata\u003c/strong\u003e. It performs no IO and\nno non-deterministic search itself. Every edge — storage, tools, solvers, channels — is a\n\u003cstrong\u003eplugin\u003c/strong\u003e injected by the host. That\u0026rsquo;s what keeps the core deterministic and testable, and the\nsystem extensible.\u003c/p\u003e\n\u003ch2 id=\"tools--the-tool-verb\"\u003eTools — the \u003ccode\u003etool\u003c/code\u003e verb\u003c/h2\u003e\n\u003cp\u003eA block calls a tool with the plugin-neutral \u003cstrong\u003e\u003ccode\u003etool\u003c/code\u003e\u003c/strong\u003e verb — \u003ccode\u003etool \u0026quot;server\u0026quot; \u0026quot;name\u0026quot; { … }\u003c/code\u003e. The\nserver name routes to a host-injected \u003ccode\u003eToolResolver\u003c/code\u003e; nothing about the transport is baked into the\nrule:\u003c/p\u003e","title":"Plugins"},{"content":"tln-db is the Go-native embedded fact store and query engine for Tln. It sits behind the FactStore interface (see Plugins), so the language core stays storage-agnostic while tln-db provides a fast, durable, self-contained backend.\nFacts are the entity–attribute records Tln reasons over — loaded from your systems, never written in .tln. tln-db stores, indexes, and queries them.\nTwo ways to run it Embedded — a Go library, in-process (bboltstore.Open()). Sidecar — a standalone tlndb-server over gRPC (Unix socket or TCP) with an HTTP/JSON debug endpoint, so several processes can share one store (Postgres-style local socket). tln run rules.tln --store tln-db --tlndb unix:///path/to.sock What\u0026rsquo;s inside Built on proven Go building blocks, tuned for rule evaluation:\nDocument store — snappy-compressed JSON in per-tenant buckets (strict isolation), ACID, SIGKILL-durable, on bbolt (B+ tree). Inverted index — roaring-bitmap-backed lookups: equality, numeric ranges, temporal windows, group-by, closure tables, running stats (Welford), and absence queries. Vector search — per-(entity, scope) HNSW index with cosine / Euclidean distance, for the language\u0026rsquo;s find similar / retrieval needs. Composite queries — Query (pattern / predicate / or / not / full-text + aggregates + group-by), SequenceJoin, ClusterQuery, and a streamed Subscribe for reactive consumers. Queries run in two phases: narrow (intersect docID bitmaps from the inverted index) then evaluate (decode candidates and check the remaining clauses) — index-fast where it can be, exact where it must be.\nSwappable and tested tln-db ships a conformance suite that any FactStore backend runs against, so alternatives (in-memory, Pebble, …) can drop in without language-level changes. Timestamps are clock-injectable for deterministic tests, and a mutation event stream (assert / change / retract) makes changes auditable — the same determinism-and-explainability story as the language itself.\nSource: github.com/opentalon/tln-db.\n","permalink":"https://tln-lang.org/db/","summary":"\u003cp\u003e\u003ca href=\"https://github.com/opentalon/tln-db\"\u003e\u003ccode\u003etln-db\u003c/code\u003e\u003c/a\u003e is the \u003cstrong\u003eGo-native embedded fact store and query\nengine\u003c/strong\u003e for Tln. It sits behind the \u003ccode\u003eFactStore\u003c/code\u003e interface (see \u003ca href=\"/plugins/\"\u003ePlugins\u003c/a\u003e), so the\nlanguage core stays storage-agnostic while \u003ccode\u003etln-db\u003c/code\u003e provides a fast, durable, self-contained backend.\u003c/p\u003e\n\u003cp\u003eFacts are the entity–attribute records Tln reasons over — loaded from your systems, never written\nin \u003ccode\u003e.tln\u003c/code\u003e. \u003ccode\u003etln-db\u003c/code\u003e stores, indexes, and queries them.\u003c/p\u003e\n\u003ch2 id=\"two-ways-to-run-it\"\u003eTwo ways to run it\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eEmbedded\u003c/strong\u003e — a Go library, in-process (\u003ccode\u003ebboltstore.Open()\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSidecar\u003c/strong\u003e — a standalone \u003ccode\u003etlndb-server\u003c/code\u003e over gRPC (Unix socket or TCP) with an HTTP/JSON debug\nendpoint, so several processes can share one store (Postgres-style local socket).\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003etln run rules.tln --store tln-db --tlndb unix:///path/to.sock\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"whats-inside\"\u003eWhat\u0026rsquo;s inside\u003c/h2\u003e\n\u003cp\u003eBuilt on proven Go building blocks, tuned for rule evaluation:\u003c/p\u003e","title":"Tln DB"},{"content":"Tln is a language; OpenTalon is where it runs at enterprise scale. OpenTalon is an open-source, Go-built AI-orchestration platform for organizations that need AI in production — predictable behaviour, auditable boundaries, deterministic business rules, and expert-defined guardrails. Tln is its decision core.\nThe division of labour The core idea is Expert-in-the-Loop (EITL): the LLM handles conversation and intent; Tln handles knowledge and inference. Two ways Tln shows up:\nThe LLM writes Tln, not raw tool calls. Following the same insight as Cloudflare\u0026rsquo;s Code Mode for MCP, OpenTalon has the model emit Tln scenarios in a deliberately restricted DSL rather than orchestrating tool calls directly. The grammar physically cannot express unsafe operations, so the sandbox is structural — not a policy you hope the model follows. Domain experts write Tln rules and workflows. The gates, policies, and review steps that govern a decision are authored once by the people who own them, and the runtime enforces them deterministically — the same rules you\u0026rsquo;ve seen throughout these docs. So a request flows: user → core/LLM (intent) → Tln (facts, rules, decision) → plugins (act), with people reserved for the rare case that genuinely needs them.\nHuman only exceptions Channel user message LLM intent · language Tln facts · rules · decision Plugins tools · act outcome to the user Expert-in-the-Loop — the LLM handles intent, Tln makes the deterministic decision, and a human is called only for the rare exception. Every IO edge is a plugin Tln\u0026rsquo;s core is a pure language + planner with transport-free IO: it decides what should happen and returns it as data. OpenTalon provides the edges as isolated plugins, each running as a separate OS process over gRPC — a compromised or buggy plugin can never read the core\u0026rsquo;s memory, and plugins can\u0026rsquo;t call each other; only the core/LLM decides what runs next.\nTools — tln-mcp resolves tool calls over the Model Context Protocol. See MCP \u0026amp; workflows. Storage — tln-db, the Go-native fact store behind the FactStore interface (bbolt + roaring-bitmap index + HNSW vectors). Channels — Slack, HTTP, MS Teams, WebSocket, console. Security \u0026amp; retrieval — guard-llm (LLM guardrails), weaviate (RAG / vector search). The same shape as the language: a deterministic core, with tln-mcp on the tool side and tln-db on the storage side, and OpenTalon composing the rest into a production system.\nBecause OpenTalon is the host, its plugin bindings win over any connector a program declares — so the very same rules run standalone (tln run with in-source connectors) or hosted, unchanged. And since OpenTalon runs LLM-authored Tln, it sandboxes that source: env is cut and io restricted, so a generated scenario can\u0026rsquo;t reach secrets or the filesystem. See the two run modes.\nThe loop A message arrives on a channel (Slack, HTTP, …). The core/LLM interprets intent and — where a decision is needed — emits a Tln scenario. Tln reasons over facts (pulled from tln-db, RAG, or collect/enrich via MCP), applies the experts\u0026rsquo; rules, and produces a deterministic, explainable decision. Approved actions dispatch through plugins (tln-mcp tools, channels), each isolated. The result is an AI system whose decisions are reproducible, auditable, and governed by the people accountable for them — with Tln as the deterministic brain at the center.\nExplore opentalon/opentalon — the orchestration core tln-language · tln-mcp · tln-db Background: Enterprise AI Orchestration · Expert-in-the-Loop ","permalink":"https://tln-lang.org/opentalon/","summary":"\u003cp\u003eTln is a language; \u003cstrong\u003e\u003ca href=\"https://github.com/opentalon/opentalon\"\u003eOpenTalon\u003c/a\u003e\u003c/strong\u003e is where it runs at\nenterprise scale. OpenTalon is an open-source, Go-built AI-orchestration platform for\norganizations that need AI in production — predictable behaviour, auditable boundaries,\ndeterministic business rules, and expert-defined guardrails. Tln is its decision core.\u003c/p\u003e\n\u003ch2 id=\"the-division-of-labour\"\u003eThe division of labour\u003c/h2\u003e\n\u003cp\u003eThe core idea is \u003cstrong\u003eExpert-in-the-Loop (EITL)\u003c/strong\u003e: the LLM handles conversation and intent; \u003cstrong\u003eTln\nhandles knowledge and inference\u003c/strong\u003e. Two ways Tln shows up:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eThe LLM writes Tln, not raw tool calls.\u003c/strong\u003e Following the same insight as Cloudflare\u0026rsquo;s \u003cem\u003eCode\nMode for MCP\u003c/em\u003e, OpenTalon has the model emit \u003cstrong\u003eTln scenarios\u003c/strong\u003e in a deliberately restricted DSL\nrather than orchestrating tool calls directly. The grammar physically cannot express unsafe\noperations, so the sandbox is \u003cem\u003estructural\u003c/em\u003e — not a policy you hope the model follows.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDomain experts write Tln rules and workflows.\u003c/strong\u003e The gates, policies, and review steps that\ngovern a decision are authored once by the people who own them, and the runtime enforces them\ndeterministically — the same rules you\u0026rsquo;ve seen throughout these docs.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eSo a request flows: \u003cstrong\u003euser → core/LLM (intent) → Tln (facts, rules, decision) → plugins (act)\u003c/strong\u003e,\nwith people reserved for the rare case that genuinely needs them.\u003c/p\u003e","title":"Tln in production — OpenTalon"},{"content":"An agent in Tln is an automation the LLM writes once and the runtime then runs forever — deterministically, with no model in the loop at run time. A user describes a task in chat (\u0026ldquo;watch stock item ABC-123; when it drops below 10, open a refill ticket\u0026rdquo;), the LLM authors it as Tln source, and a plugin stores it and runs it autonomously.\nThat split is the whole point (see the deep dive in Deterministic Where It Matters):\nAuthoring is probabilistic — the LLM is great at turning a fuzzy request into a small Tln program. This happens once. Execution is deterministic — from then on the runtime evaluates that program on every tick, over the facts of the moment. No model call, no sampling, no re-deciding. Same facts in, same decision out. A workflow fired by a trigger Two blocks make an agent: an on trigger that watches the facts, and a workflow it fires. A workflow is a sequence of steps, each calling a tool (see Plugins). This is the real stock-watcher from opentalon-agents:\n// Fire ONCE on the downward crossing below 10 (prev \u0026gt;= 10, new \u0026lt; 10) -- // not every tick while it stays low. on change attr \u0026#34;current_stock\u0026#34; { when prev_value \u0026gt;= 10 and new_value \u0026lt; 10 workflow \u0026#34;Refill stock\u0026#34; } workflow \u0026#34;Refill stock\u0026#34; { step \u0026#34;ticket\u0026#34; { tool \u0026#34;tickets\u0026#34; \u0026#34;create\u0026#34; { title \u0026#34;Refill needed for ABC-123\u0026#34; item step(\u0026#34;trigger\u0026#34;).result.entity qty 50 } } } The on change block is edge-triggered: it fires on the moment stock crosses below 10, not on every tick while it stays low. The workflow then opens a ticket through the tickets tool — step(\u0026quot;trigger\u0026quot;).result.entity threads the item that crossed the threshold into the call.\nHow it runs — opentalon-agents opentalon-agents is the OpenTalon plugin that owns the agent lifecycle: it stores the LLM-authored Tln source and its triggers, maps incoming data to facts, keeps the fact snapshot, and records every run — all in its own store. It runs no model at run time and no scheduler of its own; it rides the host\u0026rsquo;s periodic tick and evaluates the stored source reactively against the current facts.\nuser (chat) ── create ──► opentalon-agents ──► stores Tln source + trigger host tick (every 1m) ───► opentalon-agents ──► evaluate source over facts │ on-block fires? ▼ workflow steps ──► tools (MCP / io) Authoring stays with the LLM; the decision — every time, forever — is Tln\u0026rsquo;s. That\u0026rsquo;s a deterministic agent: reproducible, auditable, and cheap to run.\nSee it in production in OpenTalon, and the full argument in Deterministic Where It Matters.\n","permalink":"https://tln-lang.org/workflows/","summary":"\u003cp\u003eAn \u003cstrong\u003eagent\u003c/strong\u003e in Tln is an automation the LLM writes \u003cem\u003eonce\u003c/em\u003e and the runtime then runs \u003cem\u003eforever\u003c/em\u003e —\ndeterministically, with no model in the loop at run time. A user describes a task in chat\n(\u003cem\u003e\u0026ldquo;watch stock item ABC-123; when it drops below 10, open a refill ticket\u0026rdquo;\u003c/em\u003e), the LLM authors it\nas \u003cstrong\u003eTln source\u003c/strong\u003e, and a plugin stores it and runs it autonomously.\u003c/p\u003e\n\u003cp\u003eThat split is the whole point (see the deep dive in\n\u003ca href=\"https://opakalex.github.io/posts/deterministic-agent-pipeline/\"\u003e\u003cstrong\u003eDeterministic Where It Matters\u003c/strong\u003e\u003c/a\u003e):\u003c/p\u003e","title":"Workflows \u0026 agents"}]