Outbound actions
Enabling an outbound action, approving a request, reading its history, turning link egress on or off, and reconciling an Outcome-unknown action.
This runbook covers the operator side of a connector runtime data source's declared actions — the only way a connector can write, invoke or publish back toward a provider or a vehicle. Everything here is off until an organization administrator turns it on, action by action.
Off by default, two separate gates. A declared action sends nothing until (1) an administrator enables that exact action definition, and (2) if the action requires it, an approver admits the specific request. For a link-destination action (a MAVLink command to a vehicle), a third, deployment-level switch — ACTION_LINK_EGRESS_ENABLED, Helm security.actionAuthority.linkEgressEnabled — must also read "true" on api-ontology, api-ingestion and API Platform, or no link-mode request is ever claimed or sent, regardless of enablement or approval.
Enabling a declared action
A data source's manifest declares its actions; none of them can send anything until enabled.
An organization admin can do this in Analytics → Ontology → Actions. The tab lists every action the installed data sources declare, with its source, its definition digest, and whether it is enabled and by whom. Enable and Disable there submit the kernel calls described under Submitting the call below; the steps are the same either way.
- Confirm the action's definition in the installed source — operation name, version, target input, and whether it carries
requiresApproval: true. Enabling binds to this exact definition digest: if the extension is upgraded and the action's definition changes, re-enable it under the new digest. - Enable it (the tab's Enable, or
action.catalog.enablefor the source and action). This is an organization-admin-only operation — an org member without that role is refused before anything is recorded. - The enablement and the digest it is bound to are written in the same audited decision that journals every other kernel write: who enabled what, and when.
To take an action back out of service, submit action.catalog.disable the same way, naming the source, the action id, its operation version and a reason. A disabled action refuses every new request immediately. A request already queued when the action is disabled, or when its definition changes, is expired when it is handed to dispatch, not sent.
A request is dispatched under a grant that lives for the action's own declared expirySeconds (at most 300). A grant not used in time expires, and the request with it, unsent.
Submitting the call
The Actions tab is the usual way to make either call. Neither has an API Platform operation. Both are kernel actions on api-ontology, POST /api/ontology/kernel/actions, which admits only a verified internal caller inside the service mesh — the ingress strips the identity headers it reads from every external request. The caller carries the administrator it acts for in the on-behalf-of headers: x-on-behalf-of-user-id, x-on-behalf-of-org-id, x-on-behalf-of-workspace-id and x-on-behalf-of-workspace-env-id.
{
"actionType": "action.catalog.enable",
"idempotencyKey": "<a fresh UUID>",
"input": {
"sourceKey": "<the installed source's key>",
"action": { "id": "…", "operation": { "name": "…", "version": 1 }, "…": "…" }
}
}input.action is the action's whole definition, copied unchanged from the installed source's manifest snapshot — not from a newer manifest file and not written by hand. Enablement binds to that definition's digest, so a definition that differs in any field enables an action nobody can request, and the real request is then refused as not enabled.
Enabling an action does not bypass approval. A requiresApproval: true action still holds every request at the approval gate below, even once enabled.
Approving a request
A request for a declared action arrives through API Platform's action.tasking.dispatch operation (POST /api/platform/v1/action/tasking/dispatch), naming the installed source, its descriptor version, the action and its parameters. If the action requires approval, the first submission records an approval request bound to that exact request and answers that approval is required. Nothing is queued yet. Enablement is checked when the request commits, before any outbound row is written: an action that is not enabled is refused then, which for an action that requires approval is the approved resubmission, not the first submission.
- Find the pending request in Analytics → Ontology → Approvals. It shows the action, the requester, when the approval expires, the targets and the parameters (a parameter you may not read is withheld).
- Approve or deny. The decision carries the exact request hash of the request you reviewed: the tab sends it for you, and a direct API caller reads it from the pending approval and cites it back. An approval is for the one exact request reviewed — never "whatever is pending under this id" — so a request that changed after it was shown to the approver cannot be approved by citing an old hash.
- The requester a reviewer must not also be is the kernel's verified actor — never a service-identity field the request happens to carry — so a request submitted through a service account is still approved or denied by a human distinct from whoever is behind that submission. The Approvals tab shows no decision buttons on your own request.
- A denial carries a reason (required, up to 2048 characters) and ends the request.
- After an approval, the requester submits the same request again, with the same idempotency key, before the approval's deadline. That submission consumes the approval and queues the action: one approval admits exactly one request. An approval not used before its deadline expires, and the request then needs a new key.
Every approval or denial is journaled the same way an enablement decision is: a DECISION receipt naming the request and the outcome, committed atomically with the request's own state change.
Reading action history and status
An outbound request's row is written only once the request has been admitted, in one transaction that steps it through requested → validated → prepared → awaiting_approval → authorized → queued (awaiting_approval only when the action required approval). A request refused or denied before that has no row; its refusal or denial is in the decision log. From queued it moves queued → dispatching → sent → accepted / in_progress / rejected → completed, ends expired if the dispatcher refuses it before sending or it waited in queued longer than its declared expirySeconds (reason queue_deadline_passed), or ends outcome_unknown when no answer settles it before its grant lapses, or no final answer follows an acknowledgement within five minutes (a correlation: "none" action ends at sent, which is final for it). Reconciliation then moves an outcome_unknown row to reconciled_succeeded or reconciled_failed.
- Status on a request is the row's current state when you read it:
queuedor later. - History is the row's own
lifecycle_history(every status change above, with its time and reason) and its attempt count (a resend adds one), plus the DECISION receipts for enablement, approval and reconciliation, and the audit of each grant issued at claim.
Analytics → Ontology → Action history lists the workspace's outbound requests newest first, filtered by status, target or source, with each row's status, requester, target and parameters. You see the parameters of your own requests; an organization admin sees every row's, and can reconcile an outcome_unknown row there. It reads api-ontology's GET /kernel/action-history. The decision log keeps the enablement, approval and reconciliation receipts.
Turning link egress on or off
ACTION_LINK_EGRESS_ENABLED is the deployment-wide switch for every link-destination outbound action — the only kind that can send a real MAVLink command to a vehicle. Two workloads read it, and both must agree:
- api-ontology reads it on every claim: with it off, a queued
linkrow is never handed to a dispatcher. - api-ingestion reads it once at start-up to decide whether its dispatcher is wired to the held links at all, and again before every send. Turning it on therefore needs an api-ingestion restart.
The Helm value security.actionAuthority.linkEgressEnabled (default false) sets ACTION_LINK_EGRESS_ENABLED=true on both Deployments together, and changing it restarts both. Set it through the chart rather than on each workload by hand, so the two cannot disagree.
| Setting | Effect |
|---|---|
ACTION_LINK_EGRESS_ENABLED unset or anything other than "true" (default) | A link-destination request is still admitted and queued, but never claimed, so it sends nothing. It does not expire: it stays queued, and becomes claimable as soon as link egress is turned on. Non-link (simulation) destinations are unaffected — and unaffected either way, since real egress for simulation destinations is disabled in code, not by this switch. |
ACTION_LINK_EGRESS_ENABLED=true on both workloads | A queued link-destination request may be claimed and sent over a signed link this deployment holds, still gated by the command allow-list and the link's own lease. Enablement and approval were checked when it was queued. |
The outbound dispatcher is a separate switch, ACTION_DISPATCH_ENABLED (off by default, Helm apiIngestion.actionDispatch.enabled) — the api-ingestion host that claims due actions, releases their credential, and sends. api-ontology signs grants with the private key in ACTION_AUTHORITY_SIGNING_JWK (Helm security.actionAuthority.signingKey.existingSecret); api-ingestion and API Platform verify them with the public keys in ACTION_AUTHORITY_VERIFICATION_JWKS (Helm security.actionAuthority.verificationKeys.existingSecret; current and previous key, for rotation). The chart refuses to render the dispatcher without both Secrets, and api-ingestion refuses to boot with the dispatcher on and no public keys, so a key rotation is a planned, verified change, not a silent one.
A link-destination request queued while link egress was off waits at most its action's expirySeconds from admission. If link egress is turned on within that time it is claimed and sent, unless its action has been disabled or redefined since; that one is expired at dispatch instead. After that time it ends expired (queue_deadline_passed) and is never sent.
To turn link egress on for a deployment:
- Confirm the dispatcher is on and both key Secrets are set, as above.
- Set
security.actionAuthority.linkEgressEnabled: truein your values and upgrade the release (for examplehelm upgrade … --set security.actionAuthority.linkEgressEnabled=true). The chart setsACTION_LINK_EGRESS_ENABLED=trueon api-ontology, api-ingestion and API Platform and restarts all three. API Platform's credential release spends each link command's grant once and audits it asaction.credential.releasedbefore the command is sent; while it reads the switch off, every link command is expired unsent ascredential_refused. - Confirm the specific action you intend to use is enabled (
action.catalog.enable) and that its command is on the allow-list — today, onlyMAV_CMD_SET_MESSAGE_INTERVAL. Every other MAVLink command is refused before it is ever encoded, regardless of this switch. The link's source must also be pinned to the extended MAVLink dialect, the one that defines COMMAND_LONG and COMMAND_ACK: a request on a link pinned to any other dialect is refused aslink_dialect_lacks_commands, never sent and never simulated. And the link must be signed (signing: 'signed-required'): an unsigned link stays receive-only, and a command for it is refused aslink_not_held. - To turn it back off, set
security.actionAuthority.linkEgressEnabled: false(the default) and upgrade the release. Alinkrow not yet claimed stays queued until itsexpirySecondsfrom admission pass, then endsexpired, unsent. A claimed command not yet sent is expired at dispatch. A command already sent and waiting for its acknowledgement gets no further retry; if no acknowledgement arrives, it settles asoutcome_unknownand must be reconciled below.
There is no tenant-level or per-action override for ACTION_LINK_EGRESS_ENABLED — it is one switch for the whole deployment. Scope what can actually send by what you enable per action, not by expecting this switch to be granular.
Reconciling an Outcome-unknown action
A link-mode command gets bounded, protocol-aware retries of the same command under the same grant while it waits for the vehicle's acknowledgement. When those retries are spent with no acknowledgement, the request settles as outcome_unknown — never silently failed — and no later action on that same target at the same destination is claimed for sending until this one is reconciled: a new request is still admitted and queued, and waits. This is deliberate: the vehicle may have executed the command and the acknowledgement was lost, or the command may never have arrived: nobody but an operator with independent evidence can tell which, and the platform will not guess.
- Identify the
outcome_unknownrow (its target, action and when it was last attempted): filter Analytics → Ontology → Action history by the status Outcome unknown. - Determine what actually happened, from evidence outside the connector runtime — telemetry that shows the commanded change took effect, a vehicle log, direct observation.
- Record the outcome, with the row's Reconcile in Action history (organization admins) or by submitting
action.outbound.reconcilenaming the request. The dialog chooses no outcome for you; choose one of:succeeded— moves the row toreconciled_succeededand releases the hold on its target.failed— moves the row toreconciled_failedand releases the hold on its target.still_unknown— writes no change at all; the hold on the target stays exactly as it was, and you can reconcile again later with better evidence.
- Include a reason. The call is audited like an approval decision — a DECISION receipt naming the request and the outcome you recorded — even a
still_unknowncall that changes nothing.
Only a row currently outcome_unknown can be reconciled; reconciling answers exactly "what actually happened to this one ambiguous attempt," never a row that was never ambiguous or was already reconciled.
Related
- Connector runtime overview — what the connector runtime is and how the outbound lifecycle fits together.
- Ingest modes — A MAVLink link over UDP — the inbound link a
link-destination action sends over. - Security → Permission model — the admin role
action.catalog.enable/disablerequire. - Security → Audit logging — where DECISION receipts and the action history underneath this runbook live.