Binding one BPMN task to an authorized service
A native host can bind one imported BPMN task to one installed service query or one authorized durable event-channel post. For example, route an eligible order with DMN, check stock through…
Binding one BPMN task to an authorized service
A native host can bind one imported BPMN task to one installed service query or one authorized durable event-channel post. For example, route an eligible order with DMN, check stock through an authorized service, retain the observation, and complete fulfillment only when that query returns success. A known failed observation can be retried explicitly; an uncertain outcome remains uncertain across restart.
This is the F8-D5/F8-D6 native Rust owner API, selected through the Host's
process-runtime capability. It does not add a desktop control, HTTP endpoint,
scheduler, or an XML serviceTask interpreter. Existing plain tasks from the
parallel or decision-routing
importer supply the task/action mapping. Import and formula evaluation grant no
service authority and never dispatch work.
Assemble the workflow
- Import the BPMN source, optionally with the supplied DMN policy and explicit routing binding. Compile its emitted source normally through LIR.
- Compile a PROCESS plan against an installed operation package. The first
profile admits exactly one operation with only
Readeffects, no responder roles, and a Boolean result. The service's installedServiceOperationsbinder must authorize the authenticated principal and model. - Prepare the native NET owner and a pure one-argument mapper that returns a VECTOR of service arguments from the marking. Use the ordinary frozen-callable admission API: existing inference restrictions remain intact.
- Open
grid_runtime_host::net_owner::task::BpmnTaskOwnerwith the model.db path, instance owner ID, authenticated principal/model, compiled modules and inventory, and the Host's existing durableProcessService. Do this before any NET firing.TaskBinding::from_manifestselects the source task and its exact begin/complete actions. Setmax_attemptsbetween one and three. - Use
fire(operation_id, expected_version, transition)for unbound steps. Usestart(expected_version)for the bound task,reconcile()to accept retained results,retry(failed_attempt_number)for an explicit retry, andcancel()to cancel the instance. Observe the existing Process service's notifications orwaitAPI before reconciling; this owner creates no polling loop.
The stock-check BPMN uses
order-routing.dmn and order-routing-binding.json. Append the
checked initializer and mapper
to its emitted source. NetOwner::prepare_with_initializer(module, "bpmn_net", "bpmn_service_initial") freezes the checked DMN entrypoint along with the native
FIRE callable. The mapper reads frozen admission inputs; it does not read live
cells or invoke a service. The retained definition rejects changed policy/source.
An illustrative installed service plan is:
USE "ops:fixtures/service@1.0.0" AS service
PROCESS check_stock(target)
status = service.read_status(target)
(status = "ready")
END
stock = check_stock("web")
That package is supplied by the maintained test provider, not a public stock
service. A deployment supplies its own installed operation descriptor and owner.
The complete executable example is
rust/grid-runtime-host/tests/bpmn_service_workflow.rs; the focused command in
tools/testing/bpmn-service/README.md runs the actual compiler, LIR, Process
service and NET owner with a local authorized provider fixture. No credentials or
external service are needed for that example.
Durable boundaries and results
| Boundary | Retained behavior |
|---|---|
| Open | Pins principal/model, task/actions, argument mapper fingerprint, NET contract, exact service plan, limits and retry bound. A different binding or a second live owner refuses. |
| Begin | Commits native task begin and mapped arguments together in model.db. Disabled/stale begins dispatch nothing. |
| Start | Persists a dispatch boundary, then uses the existing principal-scoped StartAttempt key and fingerprint. Duplicate Start retains the original root. |
| Result | Checks original model and compiled plan identity. Boolean true commits the root snapshot and native task completion in one database transaction. Boolean false or a known stopped result retains failure without completing the task. |
| Retry | Only a known failed query may create the next attempt, up to three total. Arguments stay frozen; every prior root result remains visible. Repeating the same retry request does not create another root. |
| Cancel | Persists cancellation intent before asking ProcessService to cancel and refuses further workflow firing. A late result is retained but cannot complete the cancelled task. Unknown external outcomes are never represented as confirmed nonexecution. |
| Reopen | Reads the existing binding and Process retention. A settled result completes the task once without querying again. Missing history after the dispatch boundary becomes Unknown or a fail-closed history error. |
The host must route mutations of a bound instance through this owner; exposing raw NET firing beside it would bypass the service binding. Authenticated principal selection is a host responsibility, not an authored string. The installed provider owns its authorization, durable operation protocol and body storage. The host continues to install its existing ProcessRetention store.
TaskSnapshot exposes state, cancellation intent, any local recovery failure,
attempt number/key, root ID, pinned arguments, prior RootSnapshot results and
NET version. Exact service outputs, failures, traces and identities remain in
those root snapshots. Mapped arguments are capped at 64 KiB. The task row is bounded to 512 KiB; each Process root has
a 64 KiB retained-result reservation, concurrency one and a 30-second deadline.
The NET receipt ceiling is 256 for this bounded imported graph. Budget exhaustion
refuses without partially completing a native task.
A service root must remain retained while its result is awaiting reconciliation. If an operator removes its history, the task never infers permission to resend. After terminal task reconciliation the copied result survives service-root cleanup. Reopening requires the same authority and exact definitions; it does not silently migrate an instance, reinstall a provider or grant new permissions.
Recovery scope
The adapter observes the Process service's retained status. Its ordinary record-only recovery seals an interrupted root as Unknown; an installed resumable store may expose Suspended, which remains inspectable through ProcessService until handled by the existing Process recovery owner. Reconciliation never resumes source, renews a deadline or repeats provider work. Cancellation can still stop a suspended root. A crash between the durable dispatch boundary and service admission may therefore require operator investigation even if no query reached the service.
Other writes, automatic retries, general provider compensation, service/boundary events, human tasks, subprocesses, instance migration and a remote/UI ingress are separate profiles. This slice provides a complete bounded native query or event-handoff workflow; it does not claim distributed exactly-once effects or full BPMN service orchestration.
One durable event handoff (F8-D6)
An eligible order can follow the same DMN and BPMN route, then post its request to an installed durable event channel. Task completion means the channel has committed the event. It does not mean a downstream consumer has fulfilled the order.
The native installer creates process_streams::package::receipt_package with
an exact package reference and closed ordinary-data payload type, then installs
it with BoundStreamSource::install against the original ProcessStream and
recoverable DiskProcessRetention. Supply that installed provider to the existing
ProcessService::with_retention_and_operations. Source imports never install a
channel or grant its principal/model authority. The original package contract
and read-only task profile keep their existing descriptor identities.
The event handoff plan returns
fulfillment.post(request) directly. Bind handoff_plan using the same imported
task mapping and a pure VECTOR argument mapper. F8-D6 accepts exactly this
single-operation, direct-return shape: one post, its input parameter, its
String receipt result, and no responders or extra continuation. Computation of
the request belongs in the frozen argument mapper. Broader operation packages,
stop, write-result postprocessing, and nested plans are refused.
reconcile() reads the original operation owner receipt and copies it into
TaskSnapshot.effectReceipts and the committed eventIngress ID together with
task completion in the NET transaction.
A receipt commits the exact operation, descriptor, authority/channel binding and
result/evidence body references. The original root snapshot stays visible,
including an interrupted root's stopped status. Receipt recovery never pretends
that stopped LIR source resumed.
If a write committed but its reply was lost, call recover_receipt() explicitly.
It schedules the existing ProcessService reconciliation job for the original
operation revision. Observe that service's operation inspection/notifications,
then call reconcile() to retain the result. The event owner searches its
original Backbone source log by the stable operation-derived ingress ID and
checks the timestamp and frozen payload. It reconstructs the existing operation
receipt without posting another event. A changed channel, mismatched payload,
missing log, absent event, expired lookup authority, or exhausted lookup budget
cannot authorize a retry. Unresolved outcomes stay Unknown.
A fresh attempt is allowed only after a terminal receipt proves no effect,
for example a channel closure detected before append. retry(attempt) preserves
the task's original arguments and its three-attempt maximum; it still requires
current provider admission. A closed or expired channel therefore continues to
refuse. Unknown and committed writes never create another attempt. There is no
automatic retry or generalized exactly-once promise.
Cancellation persists intent and prevents further workflow firing. The native
root generation fence excludes a post that has not crossed its commit boundary.
An already committed event is not removed. Its receipt can be recovered after
cancellation and remains visible, while the task stays cancelled. An unproved
outcome remains Unknown with cancelRequested set; cancellation is not proof
of non-execution or compensation.
Each operation has one dispatch, at most three original-receipt lookups, a 10-second write deadline bounded by root/channel expiry, and a 24-hour lookup and retention window anchored to preparation. Existing task, argument and NET bounds still apply. Retain the original root, operation journal, channel checkpoint and source log until the task is settled. Recovery creates no new worker, queue, evaluator or event journal.
Recovered stopped roots retain their original unaccepted operation history under existing PROCESS retention rules. A BPMN completion receipt is not PROCESS source acceptance and does not grant a new root-retirement exception. Explicit retirement of that stopped history is a separate ownership extension; the existing retained root capacity still applies. Normally accepted event roots use ordinary cleanup.
Waiting for consumer completion
To make downstream acceptance the completion condition, install the receipt-capable source for a channel with max_episodes = 1. Pass its completion_binding() to BpmnTaskOwner::open_with_consumer along with the same installed service. This pins the principal, model and channel contract when the task is created. Open the same binding to recover an existing task.
After the post commits, the task is AwaitingConsumer. Advance the existing stream with its explicit wake API. A later task reconcile reads the original consumed event and the terminal consumer PROCESS result. Boolean true completes the native NET transition. False or a known stopped result retains failure. An unavailable or inconsistent observation preserves uncertainty. A committed producer receipt always prevents reposting, including when downstream processing fails.
The task stores the original consumer attempt, root, terminal status and Boolean outcome with the NET transition. A cancelled task retains a late consumer result with its cancelled marking. Reopening the earlier event-acceptance profile continues to use its own persisted contract.
Payment and compensation after a completed effect
A decision-approved reservation can now wait for a payment and withdraw that reservation if its deadline expires. The native net_owner::task::settlement::SettlementOwner composes the existing decision/service task, receive owner, and a second service task for compensation. It binds the inverse operation and its installed confirmation consumer before the reservation starts.
Use these supplied models together:
- Import
service-fulfillment.bpmnwithorder-routing.dmnthroughbpmn::import_with_decision. Compile its returned source withservice-fulfillment.grid. Bind theAvailabilitytask to the receipt-capablehandoff_planinevent-fulfillment.grid. - Import
receive-with-timeout.bpmnthroughbpmn::importand compile its returned source. - Compile payment-compensation.grid. Its initializer takes the original effect record, including its event identity and receipt digest. Its mapper addresses a withdrawal to that original event.
- Supply a
CompensationPlanwith the compiled inverse module, durable ProcessService, PROCESS inventory,TaskBinding, and installedConsumerBinding. For this example the task actions arebeginandcomplete, the mapper iscompensation_arguments, and the operation plan ishandoff_plan. The consumer must apply withdrawal to the addressed reservation and return true for that specific withdrawal event. - Open
SettlementOwnerwith the forward task, receive module, inverse plan, and terminal stepsmerge_3andfinish. Route and start through this owner. Payment delivery or a timer wake-up then advances the whole workflow.
The original committed effect receipt supplies both the payment correlation key and the deadline anchor. The payment message is Payment, and its correlation value is the original process-event:… identity. The five-second interval is measured from the original write receipt's completion time. Reopening the task later preserves that timestamp.
A matching payment completes the native terminal path and retains the reservation. A committed timeout prevents subsequent payment delivery from changing the outcome and prepares the inverse task with the original receipt. finished becomes true after the inverse consumer confirms success and the native terminal path completes. The snapshot retains the original task, receive marking, inverse task, operation receipts and consumer result.
Call reconcile() after reopening or after a consumer reports completion. An interruption between timeout commit and inverse dispatch resumes the same inverse owner and arguments. recover_effect() and recover_compensation() reconcile an uncertain operation through its existing original-receipt lookup. retry_compensation(failed_attempt) requires a retained failure with no committed effect. A committed withdrawal awaiting confirmation retains its original identity. cancel_before_effect() is available while the forward task remains idle.
This is an explicit native composition of one admitted decision/service path, one payment wait and one inverse effect. Authored task compensation is available through the activity-scope profile below. Nested compensation stacks and multi-instance scopes remain on the F8 roadmap. The host supplies authenticated message ingress, timer wake-ups and installed service/consumer authority. Reading the source or compiling the modules creates no worker.
Authored task compensation in one native scope
Compensating reservations reserves a flight and a hotel, then throws a compensation event. Each service task has a compensation boundary catch associated with an isolated isForCompensation="true" handler. The imported bpmn-2-scoped-compensation-v1 profile records each task's completion order in its native marking. The hotel handler must finish before the flight handler can begin. An explicit activityRef selects one original activity; waitForCompletion="false" releases the outgoing sequence flow while retaining the outstanding compensation token. A process end still waits for outstanding handler work.
Append the native bindings to the imported source, then supply the same installed receipt-capable service and PROCESS inventory used by the event-handoff workflow:
- Open
net_owner::task::scope::ActivityScopewith the compiled module,bpmn_net,reservation_initial, and the manifest's start, throw, throw-completion and end actions as its allowed controls. - Bind
FlightandHotelwithTaskBinding::from_manifest, their argument mappers, and the authorizedhandoff_plan. Each activity keeps a distinct durable attempt identity while sharing the process marking. The scope and each live activity hold ownership leases. - Fire the admitted start action. Start and reconcile each service activity through
BpmnTaskOwner. Only the retained service result completes its native task and registers compensation. - Fire the authored compensation throw. Create each
CompensationBindingfrom the handler and throw IDs in the manifest.bind_compensationrequires the original completed activity in this same scope, its committed effect receipt, the inverse service authority, and an installed confirmation consumer. Its LIR mapper receives that original event identity and receipt digest. Caller-provided event IDs cannot redirect the inverse. - Start and reconcile the enabled handler, confirm its withdrawal through the installed consumer, then run the next handler. Fire the throw-completion and end controls once enabled.
Reopening with the same scope, activity identities, definitions and installed authority recovers retained arguments, attempts, producer receipts and consumer results. An uncertain write requires original-receipt recovery; retry requires a retained no-effect failure. Neither path creates a second evaluator or replays completed source.
Use ActivityScope::cancel to atomically drain the native instance and retain cancellation intent for all bound activities. Reconcile each activity through its existing Process owner to request cancellation and resolve late receipts. A late committed effect remains recorded even after cancellation; cancellation does not assert that the effect was undone. Bound task actions cannot also be admitted as scope controls. A competing live handle for the same activity is refused, and new activities cannot join a terminal scope.
This executable profile remains acyclic, with the existing bounds of eight flow nodes and eight sequence flows. It admits up to sixteen compensation handlers and retains at most sixty-four activity records per scope. The present importer refuses nested subprocess compensation, loops, multi-instance snapshots, transaction cancellation, mixed DMN/receive/compensation graphs and unsupported event semantics. These remain F8 work. The native checks exercise production LIR, NetOwner, TableDb and ProcessService using an installed service fixture; full Host ingress and standards conformance remain separate outstanding work.