MDK Logo

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

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

MethodExample PathmdkClient MethodPurpose
GET/auth/actionsqueryActions()Query actions by lifecycle state (voting/ready/executing/done)
POST/auth/actions/votingpushAction()Submit a single action for approval
POST/auth/actions/voting/batchpushActionsBatch()Submit multiple actions for approval
PUT/auth/actions/voting/:id/votevoteAction()Cast approval/rejection vote on an action
DELETE/auth/actions/voting/cancelcancelActionsBatch()Cancel pending actions by IDs

Plugin structure

backend/plugins/actions/
├── mdk-plugin.json
└── controllers/
    ├── query.js
    ├── push.js
    ├── push-batch.js
    ├── vote.js
    └── cancel.js

Example 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

On this page