Reactor Apps & Smart Panels
Turn logic-cell rules into live applications: JSON APIs, shareable web forms, and an encrypted per-app records store — no server code, no deployment.
Reactor builds directly on Logic Cells: a rule you can query is a rule you can serve.
The idea
You have a rule in a namespace:
quote(Country, Product, Price) :-
base_price(Product, P),
vat(Country, V),
Price is P * (1 + V).
reactor.expose turns it into:
-
a JSON API at
/apps/<app>/<slug>.api— callable by anything that speaks HTTP; -
a Smart Panel at
/servers/<app>/<slug>.sp— a branded, shareable web form for the same rule.
Requests bind the rule’s inputs, the engine solves, outputs return as JSON (or rendered results in the panel). Every execution runs through the same sandboxed query path as logic.query — an exposed rule can do exactly what a queried rule can do, nothing more.
Exposing a rule
{ "rule": "quote", "namespace": "pricing" }
That’s the common case — the contract is auto-derived from the rule head: the last argument becomes the output, the rest become inputs. To control it explicitly, pass a mode string:
{ "rule": "quote", "namespace": "pricing", "modes": "+country:atom +product:string -price:float" }
+name:type binds from the request; -name:type is collected and returned. Types: atom, string, integer, float, bool, any. The resolved contract comes back in the response along with both URLs.
Verbs and safety
-
verb: "get"— cacheable, prefetchable; for pure queries. -
verb: "post"— for anything else.
Rules whose body calls call_tool, assertz, or retract are forced to POST regardless of what you request — side-effecting endpoints must not be GET-cacheable. This is detected from the rule body, not trusted from the caller.
Auth
auth: "none" (public), "bearer" (token in the Authorization header; a token is minted and returned at expose time), or "password" (for panels — a shared secret gate on the form).
Shaping the endpoint
-
rate_limit_per_hour— throttle per caller. -
limit— cap solutions returned per request. -
single: true— return the first solution as an object instead of a list. -
requires— battery ids to hydrate before serving (usually auto-detected).
Battery dependencies
If the rule depends on battery predicates, expose hydrates the declared batteries into the namespace automatically — the endpoint works on first call.
Lifecycle: list and revoke
Exposing is only half of it — you also need to know what is live, adjust it, and be able to take it down.
{ "name": "data-grout@1/reactor.list@1", "arguments": { "auth": "none" } }
reactor.list returns one entry per exposed rule: the rule, its namespace, the owning app, verb, auth mode, the mode string, and both URLs. Filter by namespace to scope to one cell, or by auth — auth: "none" is the audit you actually want, since it names every endpoint reachable without credentials. The response also carries a public_count so the number is hard to miss. Secrets are never returned; has_bearer / has_password report only whether a secret is set.
{ "name": "data-grout@1/reactor.revoke@1", "arguments": { "rule": "quote", "namespace": "pricing" } }
Changing one setting
{ "name": "data-grout@1/reactor.update@1",
"arguments": { "rule": "quote", "namespace": "pricing", "verb": "post" } }
reactor.update changes only the fields you pass and preserves the rest. That matters because reactor.expose is a full declaration — re-exposing to flip the verb would reset an unmentioned limit back to unlimited. Update reads the current contract, applies your changes on top, and re-declares the merged result; the response lists what actually changed. Switching auth to bearer mints a fresh token and returns it once.
reactor.revoke takes the endpoint offline: the .api and .sp URLs stop resolving and any issued bearer token or panel password is invalidated immediately. It is deliberately narrow — the rule, the app, the records store, and any panels are untouched. Revoking is un-publishing, not deleting; re-run reactor.expose to bring it back (with a fresh token if you ask for one).
Both read the same source of truth the Reactor console does, so the tools and the UI can never disagree about what is published.
The records store
Reactor apps get an encrypted records store for state that doesn’t belong in the fact space — submissions, orders, profiles:
| Tool | Does |
|---|---|
store.put |
Upsert a record by key (payload encrypted at rest) |
store.get |
Fetch one record (decrypted) by key |
store.query |
List/filter by plaintext metadata containment (e.g. {country: "be"}), newest first |
store.delete |
Remove a record by key |
Records are scoped to the calling server, an optional site, and a named collection — one app can keep orders and subscribers separate, and one server can back multiple sites. Payloads are encrypted; the metadata you choose to attach is plaintext and is what store.query filters on — put only what you’re willing to index there.
A common pattern: the exposed rule handles the decision (validate, price, classify) and a follow-up call_tool to store.put inside the rule persists the outcome — the whole app is one rule plus the store.
Smart Panels
Smart Panels are the human-facing surface: a branded form generated from the endpoint’s contract, served at the .sp URL, submitting to the rule and streaming results back. Panels are built and customized in the Panel Studio (field labels, ordering, branding, result presentation) and shared by URL — visitors don’t need a DataGrout account (subject to the endpoint’s auth setting).
Panels render the same contract the API serves: one exposure, two surfaces. Anything a panel can submit, the API accepts, and vice versa.
Availability
Reactor and Smart Panels are feature-gated (per-account, or enabled site-wide by the platform). Having Reactor implies Smart Panels — a reactor app is never left API-only. If reactor.expose reports the feature is unavailable, ask your admin.
Quick reference
| Setting | Where | Values |
|---|---|---|
| Rule + namespace |
reactor.expose |
any stored rule |
| Audit what is live |
reactor.list |
filter by namespace / auth |
| Change one setting |
reactor.update |
rule + only the fields to change |
| Take an endpoint down |
reactor.revoke |
rule (+ namespace) |
| Contract |
modes |
+in:type / -out:type, auto-derived if omitted |
| Verb |
verb |
get / post (side-effecting rules forced to post) |
| Auth |
auth |
none / bearer / password |
| API URL |
response url |
/apps/<app>/<slug>.api |
| Panel URL |
response panel_url |
/servers/<app>/<slug>.sp |
| Store scoping |
store.* |
server + optional site + collection |
| Store filtering |
store.query |
plaintext metadata containment |