Write actions
How to add an approval flow to your MDK app
Overview
This guide demonstrates how to submit approval-gated write actions from a React app, review the server-side voting queue, and approve, reject, or cancel pending actions through the Gateway.
Prerequisites
- The Gateway is running with an actions plugin mounted
- The signed-in user has the Gateway
actions:wpermission - The signed-in user also has the target device-family write permissions required by the action, such as
miner:worcontainer:w - If present, your React app is wrapped in
<MdkProvider apiBaseUrl={...}> - The feature stages write actions in
actionsStorefrom@tetherto/mdk-ui-foundationor provides actions through an existing feature such as Pool Manager
Submit staged actions
1.1 Submit a single action
Use useSubmitSingleAction() when the UI lets an operator submit one staged action by id.
import { useSubmitSingleAction } from "@tetherto/mdk-react-adapter/hooks";
function SubmitActionButton({ actionId }: { actionId: number }) {
const submit = useSubmitSingleAction();
return (
<button
type="button"
disabled={!submit.canSubmit || submit.submittingActionId === actionId}
onClick={() => submit.submitSingle(actionId)}
>
Submit action
</button>
);
}1.2 Submit all staged actions
Use useSubmitPendingActions() when the UI has a review tray or bulk-submit control that should send the whole local staging queue.
import { useSubmitPendingActions } from "@tetherto/mdk-react-adapter/hooks";
function SubmitActionsButton() {
const submitPending = useSubmitPendingActions();
return (
<button
type="button"
disabled={submitPending.isSubmitting || !submitPending.canSubmit}
onClick={() => submitPending.submit()}
>
Submit staged actions
</button>
);
}Review the server-side queue
After submission, actions move from the local staging queue into the Gateway's voting surface (typically exposed by a plugin at routes like /auth/actions*).
2.1 Review with usePendingActions()
Use usePendingActions() for a pending-action review table. Pass refetchInterval to override the default poll cadence (see hook reference).
import { usePendingActions } from "@tetherto/mdk-react-adapter/hooks";
function PendingActionsList() {
const { data: pending = [], isLoading } = usePendingActions({
refetchInterval: 5000,
});
if (isLoading) return <p>Loading pending actions...</p>;
return (
<ul>
{pending.map((action) => (
<li key={action.id}>{action.id}</li>
))}
</ul>
);
}2.2 Review with useLiveActions()
Use useLiveActions() when the UI needs to separate the current user's actions from others and gate approve/reject controls on canApprove.
For polling cadence and role logic, see the hook reference.
Approve or reject an action
Use useVoteOnAction() to cast an approval or rejection. The hook calls the Gateway's voting endpoint (for example, PUT /auth/actions/voting/:id/vote if using that plugin pattern) and invalidates the relevant action caches. Disable direct vote buttons when canVote is false. Review-tray UIs that approve other users' actions should combine this mutation with useLiveActions().canApprove.
import { useVoteOnAction } from "@tetherto/mdk-react-adapter/hooks";
function VoteButtons({ actionId }: { actionId: string }) {
const vote = useVoteOnAction();
return (
<>
<button
type="button"
disabled={!vote.canVote}
onClick={() => vote.vote({ id: actionId, approve: true })}
>
Approve
</button>
<button
type="button"
disabled={!vote.canVote}
onClick={() => vote.vote({ id: actionId, approve: false })}
>
Reject
</button>
</>
);
}Cancel pending actions
Use useCancelAction() when the current operator should withdraw one or more pending actions before the vote thresholds are met. The hook calls the Gateway's cancel endpoint (for example, DELETE /auth/actions/voting/cancel if using that plugin pattern).
import { useCancelAction } from "@tetherto/mdk-react-adapter/hooks";
function CancelActionButton({ actionId }: { actionId: string }) {
const cancel = useCancelAction();
return (
<button type="button" onClick={() => cancel.cancel({ ids: [actionId] })}>
Cancel
</button>
);
}Verify the result
Approved actions become command requests after the configured vote thresholds are met. Watch the feature state that initiated the
action, or poll the action list with usePendingActions() / useLiveActions() until the item leaves the voting queue.
For Pool Manager screens, use the existing actions sidebar USAGE and Pool Manager blueprint as the integration examples.
Create an actions plugin
To enable approval-gated writes, create a plugin that exposes HTTP routes for the write-action workflow. Each route should call the corresponding services.mdkClient method.
The paths shown below (/auth/actions*) are illustrative examples. You may use any path structure that fits your plugin's routing pattern.
Required routes
| Method | Example Path | mdkClient Method | Purpose |
|---|---|---|---|
GET | /auth/actions | queryActions() | Query actions by lifecycle state (voting/ready/executing/done) |
POST | /auth/actions/voting | pushAction() | Submit a single action for approval |
POST | /auth/actions/voting/batch | pushActionsBatch() | Submit multiple actions for approval |
PUT | /auth/actions/voting/:id/vote | voteAction() | Cast approval/rejection vote on an action |
DELETE | /auth/actions/voting/cancel | cancelActionsBatch() | Cancel pending actions by IDs |
Plugin structure
backend/plugins/actions/
├── mdk-plugin.json
└── controllers/
├── query.js
├── push.js
├── push-batch.js
├── vote.js
└── cancel.jsExample controller (push.js)
'use strict'
module.exports = async function pushAction (req, services) {
const { mdkClient } = services
if (!mdkClient) throw new Error('ERR_KERNEL_CLIENT_NOT_CONNECTED')
// Extract auth info (requires auth plugin or equivalent)
const voter = req._info.user.metadata.email
const authPerms = req._info.permissions || []
return await mdkClient.pushAction({
query: req.body.query, // Device query/selector
action: req.body.action, // Action name from worker contract
params: req.body.params, // Action parameters
voter, // Current user identifier
authPerms // User's permissions (e.g., ['miner:w'])
})
}Manifest example (mdk-plugin.json)
{
"name": "@yourorg/mdk-plugin-actions",
"version": "1.0.0",
"description": "Approval-gated write action APIs",
"routes": [
{
"id": "actions.push",
"handler": "./controllers/push.js",
"auth": true,
"permissions": ["actions:w"],
"http": {
"method": "POST",
"path": "/auth/actions/voting",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["query", "action", "params"],
"properties": {
"query": { "type": "object" },
"action": { "type": "string" },
"params": { "type": "array" }
}
}
}
}
}
},
"description": "Submit a write action for approval",
"safety": "Stage-only: does not execute until approved"
}
]
}Mount the plugin
const { startGateway } = require('@tetherto/mdk')
const path = require('path')
await startGateway({
kernel,
extraPluginDirs: [
path.join(__dirname, 'backend/plugins/actions')
]
})For complete mdk-client method signatures and protocol details, see the mdk-client README and Kernel actions integration tests.
Next steps
- Understand the approval-gated write architecture — including how approved actions become normal command requests
- Understand plugin permission syntax, auth, permissions, and caching
- Configure route permissions in Gateway plugins
- Review hook exports in
@tetherto/mdk-react-adapter - Run integration coverage:
backend/core/kernel/tests/integration/actions.test.js