Script Handlers
Orchestrating deterministic, multi-step workflows with SPARQL scripts.
Script handlers let you bundle complex work into a single action that RARS can invoke like any other action. They execute SPARQL scripts that can query the graph, invoke other actions, and compose results. This guide walks through building script handler actions from simple graph queries to multi-step pipelines.
Why Script Handlers
RARS already composes its own workflows by chaining action invocations during reasoning. Script handlers give you a way to pre-package operational logic into a reusable action when you want that logic to be fixed, repeatable, and invocable by name.
A script handler is a SPARQL query that can call other actions as sub-operations. The script itself is deterministic (the query plan is fixed), but the actions it calls can be anything: API calls, AI reasoning, human approvals. The result is a single action with a single I/O contract that encapsulates whatever complexity you need.
Use script handlers when you want to abstract multi-step logic into a named, reusable operation. RARS can then reason about when to invoke it, just like any other action in the matrix.
A Simple Script Handler
Start with the simplest case: a script that looks up a task's details from the graph.
tasks:GetTaskDetails
a rars-act:Action ;
rars-act:isClassifiedAs rars-act:Query ;
rdfs:label "get task details" ;
dct:description "Retrieves the title and status of a task." ;
rars-act:subjectScheme tasks:Task ;
rars-act:subjectRequired true ;
rars-act:resultScheme tasks:Task ;
rars-act:handler tasks:GetTaskDetailsHandler .
tasks:GetTaskDetailsHandler
a rars-act:ScriptHandler ;
rars-act:script tasks:GetTaskDetailsScript .
tasks:GetTaskDetailsScript
a rars-os:Script ;
rars-os:select """
PREFIX tasks: <https://example.org/spec/tasks#>
PREFIX rars-act: <https://poliglot.io/rars/spec/actions#>
PREFIX rars-os: <https://poliglot.io/rars/spec/os#>
SELECT ?title ?status WHERE {
?_process rars-os:parent ?invocation .
?invocation rars-act:subject ?task .
?task tasks:title ?title ;
tasks:status ?status .
}
""" .The script is a SPARQL SELECT query with ?_process pre-bound to the current execution. It navigates to the invocation's subject and extracts properties. The result variables are bound to the caller's result pattern positionally.
A caller invokes this as:
(?title ?status) tasks:GetTaskDetails (?task) .The first SELECT variable (?title) binds to the caller's first variable (?title), the second (?status) to the second. The variable names don't need to match -- only position matters.
Script Types and Result Binding
The script type determines how results are bound to the caller's variables. Each type has specific binding mechanics.
SELECT: Positional Variable Binding
SELECT results are mapped positionally: the first SELECT variable binds to the first result variable, the second to the second, and so on. Each result row becomes a separate binding, so a SELECT that returns 5 rows produces 5 bindings in the caller (like a join).
# Script returns ?name and ?email per row
SELECT ?name ?email WHERE { ... }
# Caller gets one binding per row
(?n ?e) people:ListPeople (?workspace) .
# ?name -> ?n, ?email -> ?e, for each personIf the caller has fewer variables than the SELECT, extra SELECT variables are ignored. If the caller has more, the extra variables are unbound.
CONSTRUCT and DESCRIBE: Named Graph Output
CONSTRUCT and DESCRIBE scripts output their triples into a named graph. The graph URI (auto-generated as urn:graph:{uuid}) is bound to the first result variable. The triples are stored in that graph, isolated from the default graph.
rars-os:construct """
CONSTRUCT {
?summary a tasks:StatusSummary ;
tasks:totalOpen ?open ;
tasks:totalClosed ?closed .
} WHERE { ... }
"""The caller receives the graph URI, not the individual triples. To access the constructed data, query the named graph:
# Invoke the action, get the graph URI
?graph tasks:SummarizeStatus (?workspace) .
# Query the named graph for constructed triples
GRAPH ?graph {
?summary tasks:totalOpen ?open ;
tasks:totalClosed ?closed .
}This isolation is intentional. Constructed output doesn't automatically merge into the default graph. It's stored in a named graph where it can be queried, validated, or selectively committed. See Reconciliation for how to manage what gets committed to the default graph.
ASK: Boolean Result
ASK scripts bind a boolean literal (true or false) to the first result variable:
rars-os:ask """
ASK WHERE {
?_process rars-os:parent ?invocation .
?invocation rars-act:subject ?task .
?task tasks:status tasks:Completed .
}
"""JSON: String Result
JSON scripts bind a JSON string literal to the first result variable. The JSON-WHERE syntax constructs a JSON object from SPARQL bindings:
rars-os:json """
JSON {
"total": ?total,
"statuses": [{
"name": ?status,
"count": ?count
}] WHERE {
?task tasks:status ?status .
}
} WHERE {
SELECT (COUNT(?t) AS ?total) WHERE { ?t a tasks:Task }
}
"""Invoking Other Actions from Scripts
Actions are invoked within scripts using SPARQL property function syntax. The result binds to the left-hand variable, the action URI is the predicate, and the arguments are in a list on the right.
Subject-Only Invocation
When the action has rars-act:subjectRequired true and no payload is needed:
?result tasks:GetTaskDetails (?task) .?task is the subject. ?result binds to the action's result.
Payload-Only Invocation
When the action has no subject (like a creation action), arguments are property-value pairs:
?result tasks:CreateTask (
tasks:title "Review Q3 metrics"
tasks:priority "high"
tasks:assignee ?currentUser
) .Each pair is a predicate value. The payload is constructed from these pairs and validated against the action's rars-act:payloadScheme before the handler executes.
Subject with Payload
When both are present, the subject is always the first argument, followed by property-value pairs:
?result tasks:UpdateTaskStatus (
?task # Subject (first argument)
tasks:status tasks:InProgress # Payload property
tasks:note "Starting work" # Payload property
) .How RARS distinguishes subject from payload: if the action declares rars-act:subjectRequired true, the first argument is always the subject. If the action has rars-act:subjectScheme but subject is optional, RARS uses argument parity (odd count means subject is present, even count means all pairs are payload).
Result Binding
The result variable (?result on the left-hand side) binds to the action's output. If the action returns multiple results, the variable binds once per result, producing multiple solution rows (like a join in SPARQL).
Multi-Step Workflows
Scripts thread results from one action into the next, building pipelines where each step depends on the previous one.
Example: Customer Onboarding
This script orchestrates four steps: create the account, provision access, send a welcome notification, and return a summary.
crm:OnboardCustomer
a rars-act:Action ;
rars-act:isClassifiedAs rars-act:Mutation ;
rdfs:label "onboard customer" ;
dct:description "Creates a customer account, provisions workspace access, and sends a welcome notification." ;
rars-act:payloadScheme crm:OnboardCustomerPayload ;
rars-act:resultScheme crm:Customer ;
rars-act:handler crm:OnboardCustomerHandler .
crm:OnboardCustomerHandler
a rars-act:ScriptHandler ;
rars-act:script crm:OnboardCustomerScript .The script:
crm:OnboardCustomerScript
a rars-os:Script ;
rars-os:construct """
PREFIX crm: <https://acme.com/spec/crm#>
PREFIX rars-ws: <https://poliglot.io/rars/spec/workspace#>
PREFIX notify: <https://acme.com/spec/notifications#>
PREFIX rars-act: <https://poliglot.io/rars/spec/actions#>
PREFIX rars-os: <https://poliglot.io/rars/spec/os#>
CONSTRUCT {
?customer crm:name ?name ;
crm:email ?email ;
crm:workspace ?workspace ;
crm:onboardedAt ?now .
}
WHERE {
# Access the payload
?_process rars-os:parent ?invocation .
?invocation rars-act:payload ?payload .
?payload crm:name ?name .
?payload crm:email ?email .
?payload crm:plan ?plan .
# Step 1: Create the customer in the CRM backend
?customer crm:CreateCustomer (
crm:name ?name
crm:email ?email
crm:plan ?plan
) .
# Step 2: Provision a workspace for the customer
?workspace rars-ws:CreateWorkspace (
rars-ws:name ?name
rars-ws:owner ?customer
) .
# Step 3: Send welcome notification
?notification notify:SendNotification (
notify:recipient ?email
notify:template notify:WelcomeTemplate
notify:context ?customer
) .
BIND(NOW() AS ?now)
}
""" .Each step is a separate action with its own handler (likely all ServiceIntegrations calling different backends). The script threads ?customer from step 1 into step 2 (as the workspace owner) and step 3 (as the notification context). The CONSTRUCT outputs a named graph containing the summary triples. The caller receives the graph URI and can query it for the constructed data.
Example: Blended Deterministic and Agentic Workflow
Scripts are how you compose deterministic and non-deterministic operations. Some steps are API calls, some are AI reasoning, some need human judgment:
wo:ExecuteWorkOrderScript
a rars-os:Script ;
rars-os:construct """
PREFIX wo: <https://example.org/spec/facilities#>
PREFIX rars-act: <https://poliglot.io/rars/spec/actions#>
PREFIX rars-os: <https://poliglot.io/rars/spec/os#>
CONSTRUCT {
?workOrder wo:status ?finalStatus ;
wo:riskLevel ?riskLevel ;
wo:approvedBy ?approver ;
wo:dispatchRef ?dispatchRef .
}
WHERE {
?_process rars-os:parent ?invocation .
?invocation rars-act:subject ?workOrder .
# Step 1 (ServiceIntegration): Fetch current details from backend
?details wo:GetWorkOrder (?workOrder) .
# Step 2 (AgenticHandler): AI assesses risk based on details + site history
?assessment wo:AssessRisk (?workOrder) .
?assessment wo:riskLevel ?riskLevel .
# Step 3 (HumanInTheLoop): Manager reviews and approves
?approval wo:RequestApproval (
?workOrder
wo:assessment ?assessment
) .
?approval wo:approvedBy ?approver .
# Step 4 (ServiceIntegration): Dispatch the approved work order
?dispatch wo:DispatchWorkOrder (
?workOrder
wo:approval ?approval
wo:priority ?riskLevel
) .
?dispatch wo:dispatchRef ?dispatchRef .
BIND(wo:Dispatched AS ?finalStatus)
}
""" .The caller doesn't know or care that step 2 is AI reasoning and step 3 is a human approval. They invoke ExecuteWorkOrder and get a work order with updated status, risk level, approval, and dispatch reference.
Conditional Logic
SPARQL's OPTIONAL and FILTER provide conditional execution within scripts.
Optional Steps
Use OPTIONAL for steps that should proceed even if they fail or return nothing:
# Always fetch the task
?task tasks:GetTask (?taskId) .
# Try to enrich with external data, but continue if unavailable
OPTIONAL {
?enrichment tasks:EnrichFromCRM (?task) .
?enrichment tasks:customerTier ?tier .
}
# Use the tier if available, default otherwise
BIND(COALESCE(?tier, "standard") AS ?effectiveTier)Conditional Branching
Use FILTER for conditional paths:
?task tasks:GetTask (?taskId) .
?task tasks:priority ?priority .
# Only escalate high-priority tasks
OPTIONAL {
FILTER(?priority = "critical")
?escalation tasks:EscalateTask (?task) .
}Reconciliation
Script handlers that produce output which supersedes previous data can use rars-act:onReconcile to retract stale statements. This is essential for keeping the default graph in sync with external sources of truth, since observations accumulate by default. See the Reconciliation guide for the full mechanics, patterns, and edge cases.
Execution Context
Scripts execute with one pre-bound variable: ?_process, which is the current rars-os:ScriptExecution. From there you navigate the process hierarchy to reach the invocation, subject, and payload:
?_process rars-os:parent ?invocation . # The action invocation
?invocation rars-act:subject ?subject . # The subject (if present)
?invocation rars-act:payload ?payload . # The payload entity
?payload tasks:title ?title . # A property on the payload?_process is always bound. Everything else is reached by traversing the graph from it.
When to Use Script Handlers
Script handlers are one tool in the toolbox. RARS can compose its own workflows at runtime by chaining actions during reasoning. Script handlers are useful when you want to:
- Fix a known sequence: the steps are well-understood and shouldn't vary. A sync workflow that always fetches, transforms, and writes should produce the same results every time.
- Abstract complexity: bundle multi-step logic behind a single action name so RARS (and other actions) can invoke it without knowing the internals.
- Blend handler types: combine deterministic service calls, AI reasoning, and human approvals in one coordinated flow.
- Expose computed data: run a graph query or aggregation as a named, cacheable action.
You don't need script handlers for everything. A single API call is a service integration. A task that requires flexible, context-dependent reasoning is an agentic handler. Script handlers fill the space where you want deterministic composition of other actions.
Tips
Thread Results Through Variables
Every action invocation binds its result to a variable. Use those variables to pass data between steps. This makes the data flow visible: you can read the script top to bottom and see what each step produces and what consumes it.
CONSTRUCT vs SELECT
Use CONSTRUCT when your result is a structured RDF resource with multiple properties. Use SELECT for tabular data (counts, lists of values). CONSTRUCT gives you a proper resource that RARS can validate against your result schema's SHACL shape.
See Also
- Service Integrations: action fundamentals, I/O contracts, subject dispatch, caching
- Agentic Actions: when AI reasoning determines the execution path
- RDF Functions: ValueFunction, JSONFunction, RDFFunction used within scripts
- Named Graphs: how CONSTRUCT/DESCRIBE output is stored and queried
- SPARQL 1.1 Query Language: the W3C SPARQL standard