npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@agent-action-runner/core

v0.8.2

Published

Agent-safe action and workflow runner core for TypeScript backends with schema validation, approval, and audit.

Readme

@agent-action-runner/core

Framework-agnostic action registry and JSON workflow runner for TypeScript backends.

Use this package when you want an agent to call existing service logic through named, schema-validated actions instead of giving the agent direct database access, internal API access, or arbitrary code execution.

Core does not execute agent-generated TypeScript. It only runs handlers that your application registered as actions.

Experimental / pre-1.0. Public APIs may change while the action, workflow, and approval contracts settle.

Install

npm install @agent-action-runner/core zod

zod is a peer dependency because action input and output schemas are part of the public action contract.

What It Provides

  • In-memory action registry.
  • Sequential JSON workflow execution.
  • Zod input and output validation.
  • Action modes: read, draft, dryRun, mutate.
  • Server-controlled mode enforcement.
  • Policy, approval, and audit hooks.
  • Audit payload minimization for input, output, and error data.
  • Deterministic input hashing for approval checks.
  • Public stable hash helper for approval services.
  • Audit store helper for persistent audit adapters.
  • Restricted step output references.
  • Type-safe workflow authoring helpers.
  • Static workflow validation helpers.
  • Action metadata for API reuse documentation.
  • Workflow retry, timeout, and continue-on-error controls.
  • Idempotency key propagation for retry-safe mutation handlers.
  • Cooperative AbortSignal for cancellable handlers.
  • Small policy composition helpers.

Quickstart

import { createRunner, fromStep } from '@agent-action-runner/core';
import { z } from 'zod';

const runner = createRunner({
  audit: async (event) => {
    console.log(event.actionName, event.status);
  },
});

runner.registerAction({
  name: 'delivery.searchJobs',
  mode: 'read',
  description: 'Search delivery jobs by status.',
  inputSchema: z.object({
    status: z.array(z.string()),
  }),
  outputSchema: z.object({
    jobIds: z.array(z.string()),
  }),
  handler: async (input) => {
    return { jobIds: input.status.includes('FAILED') ? ['job_1'] : [] };
  },
});

runner.registerAction({
  name: 'delivery.dryRunRetry',
  mode: 'dryRun',
  description: 'Validate retry candidates before mutation.',
  inputSchema: z.object({
    jobIds: z.array(z.string()),
  }),
  outputSchema: z.object({
    retryable: z.array(z.string()),
    blocked: z.array(z.string()),
  }),
  handler: async (input) => {
    return { retryable: input.jobIds, blocked: [] };
  },
});

const result = await runner.executeWorkflow({
  userId: 'operator_1',
  workflow: {
    workflowName: 'retry-failed-delivery-jobs',
    steps: [
      {
        id: 'jobs',
        action: 'delivery.searchJobs',
        input: { status: ['FAILED'] },
      },
      {
        id: 'dryRun',
        action: 'delivery.dryRunRetry',
        input: {
          jobIds: fromStep('jobs', '/jobIds'),
        },
      },
    ],
  },
});

console.log(result.outputByStep.dryRun);

Action Modes

type ActionMode = 'read' | 'draft' | 'dryRun' | 'mutate';

| Mode | Intended Use | |---|---| | read | Query or inspect existing state. | | draft | Generate a draft without changing production state. | | dryRun | Validate a future mutation and calculate impact. | | mutate | Change production state. Requires explicit mode allowance and approval. |

The default allowed modes are read, draft, and dryRun. mutate is blocked unless the execution request includes it in allowedModes.

Registering Actions

runner.registerAction({
  name: 'report.generateDraft',
  mode: 'draft',
  description: 'Create a report draft for review.',
  tags: ['reports'],
  resourceType: 'report',
  riskLevel: 'low',
  examples: [
    {
      title: 'Create a report draft',
      input: { reportId: 'report_1' },
    },
  ],
  inputSchema: z.object({
    reportId: z.string(),
  }),
  outputSchema: z.object({
    draftId: z.string(),
  }),
  handler: async (input, ctx) => {
    return {
      draftId: `${ctx.userId}:${input.reportId}`,
    };
  },
});

Action names must be unique. Handlers receive parsed input and an execution context with user id, action name, mode, workflow id, step id, metadata, approval token, approval context, cooperative AbortSignal, and optional idempotency key.

Executing One Action

const result = await runner.executeAction({
  userId: 'operator_1',
  action: 'delivery.searchJobs',
  input: {
    status: ['FAILED'],
  },
});

Use metadata for request-scoped values that are useful to policies, handlers, or audit hooks.

await runner.executeAction({
  userId: 'operator_1',
  action: 'delivery.searchJobs',
  input: { status: ['FAILED'] },
  metadata: {
    requestId: 'req_123',
    source: 'admin-console',
  },
});

Use idempotencyKey for mutate actions that may be retried or may time out from the runner's perspective while the underlying work continues:

await runner.executeAction({
  userId: 'operator_1',
  action: 'delivery.executeRetry',
  input: { jobIds: ['job_1'] },
  allowedModes: ['mutate'],
  approvalToken,
  approvalContext,
  idempotencyKey: `retry:job_1:${dryRunHash}`,
});

The raw key is available to the handler as ctx.idempotencyKey. Audit events only receive idempotencyKeyHash, a SHA-256 fingerprint for correlation. Core does not generate keys, reserve keys, lock rows, or replay results; applications own that behavior in their service or transaction layer.

Cooperative Cancellation

Handlers receive ctx.signal, an AbortSignal for cooperative cancellation.

runner.registerAction({
  name: 'delivery.searchJobs',
  mode: 'read',
  handler: async (input, ctx) => {
    return deliveryService.searchJobs(input, {
      signal: ctx.signal,
    });
  },
});

When timeoutMs expires, the runner rejects the attempt with ActionTimeoutError and aborts ctx.signal. This does not forcibly stop work already running in Node.js. Cancellation only happens if your handler passes the signal to APIs that honor it.

You can also pass a signal when executing a workflow. The runner forwards that signal to each step action and observes it during retry delays.

const controller = new AbortController();

await runner.executeWorkflow({
  userId: 'operator_1',
  workflow,
  signal: controller.signal,
});

If the workflow signal is aborted during a retry delay, the workflow fails with WorkflowAbortedError. Action handlers still use cooperative cancellation; a handler that ignores ctx.signal is not forcibly stopped.

Executing Workflows

Workflows are plain JSON data. They execute sequentially, and each step can reference outputs from earlier steps.

{
  "workflowName": "retry-failed-delivery-jobs",
  "steps": [
    {
      "id": "jobs",
      "action": "delivery.searchJobs",
      "input": {
        "status": ["FAILED"]
      },
      "timeoutMs": 1000,
      "idempotencyKey": "search-failed:2026-05-06",
      "retry": {
        "maxAttempts": 2,
        "delayMs": 50
      }
    },
    {
      "id": "dryRun",
      "action": "delivery.dryRunRetry",
      "input": {
        "jobIds": {
          "$fromStep": "jobs",
          "path": "/jobIds"
        }
      }
    }
  ]
}

Use fromStep(stepId, path) in TypeScript to create the same reference object:

fromStep('jobs', '/jobIds');

Paths are JSON Pointer strings. References can only resolve against previous step outputs.

retry.maxAttempts includes the first attempt. timeoutMs marks the attempt as failed after the configured duration; it does not cancel underlying Node.js work that has already started. idempotencyKey must be a non-empty string when present and is passed to the step's action context. Use continueOnError: true only when downstream steps can safely consume a failed step result.

For mutate actions, retries should only be enabled when the underlying service is idempotent or protected by a transaction/idempotency key. A timeout can happen while the original work is still running.

Workflow Builder

The builder gives TypeScript checks for action inputs and previous step references while still producing the same JSON workflow definition.

import {
  createRunner,
  defineAction,
  defineActionCatalog,
  defineWorkflow,
  registerActionCatalog,
} from '@agent-action-runner/core';
import { z } from 'zod';

const runner = createRunner();

const actions = defineActionCatalog({
  searchJobs: defineAction({
    name: 'delivery.searchJobs',
    mode: 'read',
    inputSchema: z.object({ status: z.array(z.string()) }),
    outputSchema: z.object({ jobIds: z.array(z.string()) }),
    handler: async () => ({ jobIds: ['job_1'] }),
  }),
  dryRunRetry: defineAction({
    name: 'delivery.dryRunRetry',
    mode: 'dryRun',
    inputSchema: z.object({ jobIds: z.array(z.string()) }),
    outputSchema: z.object({ retryable: z.array(z.string()) }),
    handler: async (input) => ({ retryable: input.jobIds }),
  }),
});

registerActionCatalog(runner, actions);

const workflow = defineWorkflow('retry-failed-jobs')
  .step('jobs', actions.searchJobs, { status: ['FAILED'] })
  .step('dryRun', actions.dryRunRetry, ({ fromStep }) => ({
    jobIds: fromStep('jobs', '/jobIds'),
  }))
  .build();

The builder does not execute TypeScript code. It only creates a WorkflowDefinition for the existing JSON runner.

Workflow Validation

Use validateWorkflowDefinition() before executing workflow JSON from files, CLI input, or generated agent plans.

executeWorkflow() also runs this validation before any step handler starts and throws WorkflowValidationError when the workflow is invalid.

import { validateWorkflowDefinition } from '@agent-action-runner/core';

const result = validateWorkflowDefinition(workflow, {
  actions: runner.listActions().map((action) => ({
    name: action.name,
    mode: action.mode,
  })),
});

if (!result.valid) {
  console.error(result.issues);
}

Validation catches:

  • missing or invalid workflow names
  • missing steps
  • duplicate step ids
  • unknown actions when an action catalog is supplied
  • invalid action modes
  • step allowedModes that exclude the known action mode
  • invalid retry, timeout, or continue-on-error controls
  • invalid idempotency keys
  • references to missing or future steps
  • unsupported input values

Approval Model

mutate actions require both:

  • mutate in the execution allowedModes
  • an approval hook result of { approved: true }
const runner = createRunner({
  approval: async ({ approvalToken, approvalContext }) => {
    return verifyApprovalToken(approvalToken, approvalContext)
      ? { approved: true, approvalId: 'approval_1' }
      : { approved: false, reason: 'Invalid approval token.' };
  },
});

The approval hook receives a normalized context:

type ApprovalContext = {
  userId: string;
  actionName: string;
  mode: 'read' | 'draft' | 'dryRun' | 'mutate';
  inputHash: string;
  resourceIds?: readonly string[];
  dryRunHash?: string;
  expiresAt?: string;
  workflowId?: string;
  stepId?: string;
};

Core does not issue or sign approval tokens. Applications should bind approval tokens to the approval context fields they care about, especially userId, actionName, inputHash, resourceIds, dryRunHash, and expiresAt.

Use createStableHash() when an approval service needs to calculate the same deterministic input hash as the runner:

import { createStableHash } from '@agent-action-runner/core';

const inputHash = createStableHash({
  userId: 'user_2',
  reason: 'Repeated policy violations.',
  dryRunHash: 'dry_run_hash',
});

The stable hash is based on normalized JSON-compatible input. Object properties with undefined values are treated as absent, so {} and { optional: undefined } hash the same. In arrays, undefined is normalized like null to preserve array positions.

Inside a handler, call ctx.requireApproval() before performing a sensitive mutation when you want an explicit guard at the mutation point. This is a defense-in-depth check for actions that were already approved through mutate mode or approvalRequired; it does not start an interactive approval flow by itself.

handler: async (input, ctx) => {
  ctx.requireApproval();
  return deliveryService.executeRetry(input.jobIds);
}

Policy Hook

Use the policy hook for application-specific allow/deny checks before the handler runs.

const runner = createRunner({
  policy: async ({ action, context }) => {
    if (action.mode === 'mutate' && context.metadata.environment !== 'staging') {
      return { allowed: false, reason: 'Mutations are disabled outside staging.' };
    }

    return { allowed: true };
  },
});

Small policy helpers are available when role/scope checks can be read from context.metadata.

import {
  allowModes,
  composePolicies,
  requireRole,
  requireScope,
} from '@agent-action-runner/core';

const runner = createRunner({
  policy: composePolicies(
    allowModes(['read', 'draft', 'dryRun']),
    requireRole('admin', { actions: ['admin.disableUser'] }),
    requireScope('delivery:write', { actions: ['delivery.executeRetry'] }),
  ),
});

requireRole() reads metadata.roles by default and requireScope() reads metadata.scopes by default. Both accept either a string or string array, and both support metadataKey when your application uses different metadata fields.

Audit Hook

The audit hook receives started, succeeded, and failed events.

Workflow retries add attempt and maxAttempts to audit events.

Audit events do not include the raw approvalToken or raw idempotencyKey. When present, the event includes approvalTokenHash and idempotencyKeyHash instead.

approvalTokenHash is a redacted fingerprint for audit correlation, not a secure approval token store. Approval services should use secret-backed HMACs or sufficiently random approval tokens for verification.

By default, audit payload behavior remains compatible with previous releases:

const runner = createRunner({
  auditDefaults: {
    input: 'full',
    output: 'full',
    error: 'full',
  },
});

For production systems, minimize stored payloads:

const runner = createRunner({
  auditDefaults: {
    input: 'hash',
    output: 'summary',
    error: 'summary',
    redactPaths: ['/password', '/token', '/secret'],
  },
  audit: createAuditHook(auditStore),
});

Actions can override runner defaults with auditPolicy:

runner.registerAction({
  name: 'admin.disableUser',
  mode: 'mutate',
  approvalRequired: true,
  auditPolicy: {
    input: 'hash',
    output: 'summary',
    error: 'summary',
    redactPaths: ['/reason', '/profile/email'],
  },
  handler: async (input, ctx) => {
    ctx.requireApproval();
    return adminService.disableUser(input.userId, input.reason);
  },
});

redactPaths uses exact JSON Pointer paths, such as /password, /profile/email, or /items/0/token. Missing paths are ignored. Wildcards, globs, and regex paths are not currently supported.

Payload modes:

| Field | Modes | |---|---| | input | full, redacted, hash, omit | | output | full, redacted, summary, hash, omit | | error | full, redacted, summary, omit |

hash stores { hash } after redaction using createStableHash(). output: 'summary' stores only outputSummary. error: 'summary' stores { name, message } and omits stack/cause fields.

When no custom summarizeOutput hook is configured, output: 'summary' falls back to a safe shape summary such as object, array(length=3), or string; it does not JSON-stringify the full output payload. If you keep error: 'full' while using redactPaths, Error objects may be cloned into serializable objects containing name, message, and stack. Production systems should prefer error: 'summary'.

import { createAuditHook, createRunner, type AuditStore } from '@agent-action-runner/core';

const auditStore: AuditStore = {
  async write(event) {
    await persistentAuditStore.append({
      executionId: event.executionId,
      workflowId: event.workflowId,
      stepId: event.stepId,
      userId: event.userId,
      actionName: event.actionName,
      mode: event.mode,
      status: event.status,
      approvalTokenHash: event.approvalTokenHash,
      createdAt: event.createdAt.toISOString(),
    });
  },
};

const runner = createRunner({
  audit: createAuditHook(auditStore),
});

Use summarizeOutput when you want compact audit summaries instead of storing full output payloads.

Errors

The core package exports typed errors for common failure paths:

  • ActionAlreadyRegisteredError
  • ActionNotFoundError
  • ActionTimeoutError
  • ModeNotAllowedError
  • PolicyRejectedError
  • ApprovalRequiredError
  • SchemaValidationError
  • InvalidStepReferenceError
  • DuplicateWorkflowStepError
  • InvalidAuditPolicyError
  • WorkflowExecutionError
  • WorkflowValidationError

Public API

Common exports:

  • createRunner
  • AgentActionRunner
  • fromStep
  • defineAction
  • defineActionCatalog
  • registerActionCatalog
  • defineWorkflow
  • validateWorkflowDefinition
  • createStableHash
  • createAuditHook
  • composePolicies
  • allowModes
  • requireRole
  • requireScope
  • core types such as ActionDefinition, ActionExample, ActionRiskLevel, AuditPayloadPolicy, AuditPayloadMode, WorkflowDefinition, ActionMode, AgentExecutionContext, ApprovalContext, AuditStore

Examples

  • examples/basic
  • examples/cli-basic
  • examples/delivery-ops
  • examples/express-admin-ops
  • examples/nestjs-admin-ops
  • examples/fastify-admin-ops
  • examples/persistent-admin-ops

License

Apache-2.0