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’t fire an action, because the language has no notion of “an action” or of “changing.” In practice you bolt on non-standard extensions and hand-roll all the orchestration yourself.
Tln 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’s what powers OpenTalon in production.
React to a fact change → call a tool
When an item’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.
% No tool calls, no "on change" trigger. Even with non-standard
% SWI extensions, the orchestration is hand-rolled:
:- use_module(library(process)).
refill(Id) :-
process_create(path(inventory_cli),
['create-refill-order', '--item', Id, '--qty', '5'],
[process(_)]).
% ...and you must notice the stock-out and call refill/1
% yourself. There is no declarative trigger on a fact
% changing to 0.on change attr "current_stock" to 0 {
logger.warn "stock-out detected for item {event.entity}"
workflow "Refill stock"
}
workflow "Refill stock" {
step "reorder" {
tool "inventory" "create-refill-order" {
item_id step("trigger").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.
collect "Failure training data" {
schedule weekly
tool "inventory" "list-items" {
query "status:defective"
per_page 100
on_error {
retry 3 times
then log "collect failed: {error}"
then skip
}
}
store results as training_facts tag "failure_training"
}
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:
detect "Overdue for service" {
for records where type == "vehicle"
and attr "km" > attr "last_service_km" + 20000
flag matching items
remediate {
if attr "priority" == "CRITICAL" {
tool "ops" "page_oncall" { vehicle attr "id" severity "critical" }
} else {
tool "ops" "open_ticket" { vehicle attr "id" reason "overdue service" }
}
for each channel in ["fleet-ops", "maintenance"] {
tool "slack" "notify" { channel channel text "Vehicle {item.id} overdue for service" }
}
}
}
How the tools connect — the plugin system
The calls above use the plugin-neutral tool verb — tool "server" "name" {…} — and don’t
hard-wire any transport. Tln’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 "inventory" … reaches an MCP server via
tln-mcp (the Model Context Protocol
over JSON-RPC), while tool "io" "writeln" reaches the built-in
io-tln plugin. Neither is baked into the language.
In the language you simply name a server and a tool — the transport is the host’s concern:
workflow "Notify low stock" {
step "reorder" {
tool "inventory" "create-refill-order" { item_id item.id quantity 5 }
}
step "announce" {
tool "slack" "post-message" { channel "#ops" text "reordered {item.id}" }
}
}
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.
This is the tools leg of Tln’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).
The whole loop
The decision stays deterministic and explainable; the acting — paging, ticketing, notifying, reordering — happens through MCP tools the rule names directly. That’s the whole loop: facts in, a deterministic decision, and an action out — none of which ISO Prolog can express on its own.