Browse documentation
Docs/Language

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

  1. Import the BPMN source, optionally with the supplied DMN policy and explicit routing binding. Compile its emitted source normally through LIR.
  2. Compile a PROCESS plan against an installed operation package. The first profile admits exactly one operation with only Read effects, no responder roles, and a Boolean result. The service's installed ServiceOperations binder must authorize the authenticated principal and model.
  3. 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.
  4. Open grid_runtime_host::net_owner::task::BpmnTaskOwner with the model.db path, instance owner ID, authenticated principal/model, compiled modules and inventory, and the Host's existing durable ProcessService. Do this before any NET firing. TaskBinding::from_manifest selects the source task and its exact begin/complete actions. Set max_attempts between one and three.
  5. Use fire(operation_id, expected_version, transition) for unbound steps. Use start(expected_version) for the bound task, reconcile() to accept retained results, retry(failed_attempt_number) for an explicit retry, and cancel() to cancel the instance. Observe the existing Process service's notifications or wait API 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.bpmn with order-routing.dmn through bpmn::import_with_decision. Compile its returned source with service-fulfillment.grid. Bind the Availability task to the receipt-capable handoff_plan in event-fulfillment.grid.
  • Import receive-with-timeout.bpmn through bpmn::import and 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 CompensationPlan with the compiled inverse module, durable ProcessService, PROCESS inventory, TaskBinding, and installed ConsumerBinding. For this example the task actions are begin and complete, the mapper is compensation_arguments, and the operation plan is handoff_plan. The consumer must apply withdrawal to the addressed reservation and return true for that specific withdrawal event.
  • Open SettlementOwner with the forward task, receive module, inverse plan, and terminal steps merge_3 and finish. 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:

  1. Open net_owner::task::scope::ActivityScope with the compiled module, bpmn_net, reservation_initial, and the manifest's start, throw, throw-completion and end actions as its allowed controls.
  2. Bind Flight and Hotel with TaskBinding::from_manifest, their argument mappers, and the authorized handoff_plan. Each activity keeps a distinct durable attempt identity while sharing the process marking. The scope and each live activity hold ownership leases.
  3. 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.
  4. Fire the authored compensation throw. Create each CompensationBinding from the handler and throw IDs in the manifest. bind_compensation requires 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.
  5. 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.