
Safe Action Advanced
- 1k installs
- Updated July 18, 2026
- next-safe-action/skills
safe-action-advanced is a Next.js agent skill that implements bind arguments, metadata, framework errors, and type utilities in next-safe-action for developers building production-grade server actions.
About
safe-action-advanced is an official skill from next-safe-action/skills for engineers extending beyond basic server action validation. It documents bind arguments via .bind() for resource IDs, typed metadata schemas consumed in middleware, framework error handling for redirect, notFound, forbidden, and unauthorized, plus InferSafeActionFnInput and InferSafeActionFnResult type utilities. Server-level action callbacks round out production patterns. Developers reach for this skill when next-safe-action actions need parameterized binds, middleware metadata, or Next.js navigation errors handled safely inside action boundaries. The skill targets TypeScript Next.js App Router projects already using next-safe-action who need advanced typing and control flow without breaking end-to-end type inference between client hooks and server handlers.
- Bind arguments support for passing extra context like resource IDs
- Typed metadata attachment for middleware and server logic
- Built-in handling of redirect, notFound, forbidden, and unauthorized framework errors
- Type inference utilities including InferSafeActionFnInput and InferSafeActionFnResult
- Server-level callbacks: onSuccess, onError, and onSettled that execute exclusively on the server
Safe Action Advanced by the numbers
- 1,025 all-time installs (skills.sh)
- +25 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #388 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/next-safe-action/skills --skill safe-action-advancedAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1k |
|---|---|
| Security audit | 3 / 3 scanners passed |
| Last updated | July 18, 2026 |
| Repository | next-safe-action/skills ↗ |
How do you handle redirect errors in server actions?
Safely implement complex server actions with bind arguments, metadata, framework errors, and server callbacks in Next.js applications.
Who is it for?
Next.js TypeScript developers extending next-safe-action with binds, metadata, framework errors, and inferred action types.
Skip if: Projects not using next-safe-action, client-only React apps without server actions, or teams needing only basic Zod validation patterns.
When should I use this skill?
The user works with next-safe-action bind arguments, metadata schemas, redirect or notFound in actions, or InferSafeActionFn types.
What you get
Typed server actions with bind arguments, metadata middleware hooks, framework error handlers, and inferred input or result types.
- typed server actions with binds
- metadata-aware middleware
- framework error handlers
Files
next-safe-action Advanced Features
Overview
| Feature | Use Case |
|---|---|
| Bind arguments | Pass extra args to actions via .bind() (e.g., resource IDs) |
| Metadata | Attach typed metadata to actions for use in middleware |
| Framework errors | Handle redirect, notFound, forbidden, unauthorized in actions |
| Type utilities | Infer types from action functions and middleware |
Server-Level Action Callbacks
The second argument to .action() accepts callbacks that run on the server (not client-side hooks):
export const createPost = authActionClient
.inputSchema(schema)
.action(
async ({ parsedInput, ctx }) => {
const post = await db.post.create(parsedInput);
return post;
},
{
onSuccess: async ({ data, parsedInput, ctx, metadata, clientInput }) => {
// Runs on the server after successful execution
await invalidateCache("posts");
},
onError: async ({ error, metadata, ctx, clientInput, bindArgsClientInputs }) => {
// error: { serverError?, validationErrors? }
await logError(error);
},
onSettled: async ({ result }) => {
// Always runs
await recordMetrics(result);
},
onNavigation: async ({ navigationKind }) => {
// Runs when a framework error (redirect, notFound, etc.) occurs
console.log("Navigation:", navigationKind);
},
}
);These are distinct from hook callbacks (useAction({ onSuccess })) — server callbacks run in the Node.js runtime, hook callbacks run in the browser.
throwServerError
Re-throw server errors instead of returning them as result.serverError:
export const myAction = actionClient
.inputSchema(schema)
.action(serverCodeFn, {
throwServerError: true,
// The handled server error (return of handleServerError) is thrown
});Bind Arguments
What Are Bind Arguments?
Bind arguments let you pass extra validated arguments to an action using .bind(). This is useful for passing resource IDs, configuration, or other data that isn't part of the form input.
Defining Bind Args
// src/app/actions.ts
"use server";
import { z } from "zod";
import { authActionClient } from "@/lib/safe-action";
export const updatePost = authActionClient
.bindArgsSchemas([
z.string().uuid(), // postId — first bind arg
])
.inputSchema(
z.object({
title: z.string().min(1),
content: z.string(),
})
)
.action(async ({ parsedInput, bindArgsParsedInputs: [postId], ctx }) => {
await db.post.update(postId, {
title: parsedInput.title,
content: parsedInput.content,
});
return { success: true };
});Using Bind Args on the Client
Call .bind(null, ...args) to create a bound version of the action:
"use client";
import { useAction } from "next-safe-action/hooks";
import { updatePost } from "@/app/actions";
export function EditPostForm({ postId }: { postId: string }) {
// Bind the postId as the first argument
const boundAction = updatePost.bind(null, postId);
const { execute, isPending } = useAction(boundAction);
return (
<form onSubmit={(e) => {
e.preventDefault();
const fd = new FormData(e.currentTarget);
execute({
title: fd.get("title") as string,
content: fd.get("content") as string,
});
}}>
<input name="title" />
<textarea name="content" />
<button disabled={isPending}>Save</button>
</form>
);
}Multiple Bind Args
export const transferItem = authActionClient
.bindArgsSchemas([
z.string().uuid(), // fromWarehouseId
z.string().uuid(), // toWarehouseId
])
.inputSchema(z.object({ itemId: z.string().uuid(), quantity: z.number().int().positive() }))
.action(async ({ parsedInput, bindArgsParsedInputs: [fromId, toId] }) => {
await db.transfer.create({
fromWarehouseId: fromId,
toWarehouseId: toId,
itemId: parsedInput.itemId,
quantity: parsedInput.quantity,
});
return { success: true };
});
// Client:
const bound = transferItem.bind(null, warehouse1.id, warehouse2.id);Bind Args Validation Errors
If bind args fail validation, ActionBindArgsValidationError is thrown. It is caught by handleServerError and returned as result.serverError.
Bind Args in Middleware
Middleware receives bindArgsClientInputs (raw, unvalidated):
.use(async ({ next, bindArgsClientInputs }) => {
// bindArgsClientInputs is unknown[] — raw client inputs
console.log("Bind args:", bindArgsClientInputs);
return next({ ctx: {} });
})Validated bind args (bindArgsParsedInputs) are available in .useValidated() middleware and in the server code function. In .use() middleware (pre-validation), only bindArgsClientInputs (raw) is available.
Framework Errors
Overview
Next.js uses thrown errors for navigation: redirect(), notFound(), forbidden(), unauthorized(). next-safe-action detects these and handles them specially:
1. Framework errors bypass handleServerError — they are not treated as server errors 2. The error is stored and re-thrown after callbacks run, so Next.js handles the navigation 3. The navigationKind is set on the result, and onNavigation callbacks fire
Using Navigation in Actions
"use server";
import { redirect } from "next/navigation";
import { notFound } from "next/navigation";
import { forbidden, unauthorized } from "next/navigation";
import { z } from "zod";
import { authActionClient } from "@/lib/safe-action";
// Redirect after creation
export const createPost = authActionClient
.inputSchema(z.object({ title: z.string(), content: z.string() }))
.action(async ({ parsedInput, ctx }) => {
const post = await db.post.create({
...parsedInput,
authorId: ctx.userId,
});
redirect(`/posts/${post.id}`);
});
// Not found
export const getPost = authActionClient
.inputSchema(z.object({ postId: z.string().uuid() }))
.action(async ({ parsedInput }) => {
const post = await db.post.findById(parsedInput.postId);
if (!post) {
notFound();
}
return post;
});
// Forbidden (403)
export const adminAction = authActionClient
.action(async ({ ctx }) => {
if (ctx.userRole !== "admin") {
forbidden();
}
return { secret: "data" };
});
// Unauthorized (401)
export const protectedAction = actionClient
.action(async () => {
const session = await getSession();
if (!session) {
unauthorized();
}
return { data: "protected" };
});NavigationKind
type NavigationKind = "redirect" | "notFound" | "forbidden" | "unauthorized" | "other";"other" covers edge cases like CSR bailout, dynamic usage errors, and React postpone.
Handling Navigation on the Client
With useAction
Navigation errors are caught, stored, and re-thrown by the hook. Next.js picks them up from the re-throw.
const { execute, hasNavigated, status } = useAction(createPost, {
onNavigation: ({ navigationKind }) => {
// Fires before the re-throw
console.log(`Navigating: ${navigationKind}`);
},
});With executeAsync
executeAsync throws on navigation errors. Use try/catch and re-throw:
const handleSubmit = async () => {
try {
const result = await executeAsync(input);
// Only reached if no navigation occurred
console.log(result.data);
} catch (e) {
// Navigation errors must propagate to Next.js
throw e;
}
};throwOnNavigation
By default, hooks catch navigation errors and fire onNavigation/onSettled callbacks. Set throwOnNavigation: true to propagate navigation errors to the nearest error boundary instead:
const { execute } = useAction(deleteAndRedirect, {
throwOnNavigation: true,
// onNavigation and onSettled are NOT available (TypeScript enforced)
onSuccess: ({ data }) => toast.success("Deleted!"),
});When throwOnNavigation: true:
- Navigation errors are thrown during React's render phase
- Next.js catches them and shows the appropriate error page (404, 403, 401)
onNavigationandonSettledcallbacks cannot be used (discriminated union type enforcement)- For side effects, use server-side action callbacks instead (see below)
See throwOnNavigation in depth for complete documentation.
Framework Errors in Middleware
Framework errors thrown in middleware are also detected. If you catch errors in middleware, always re-throw framework errors:
.use(async ({ next }) => {
try {
return await next({ ctx: {} });
} catch (error) {
// Check before swallowing!
if (error instanceof Error && "digest" in error) {
throw error; // Let Next.js handle it
}
// Handle non-framework errors
return { serverError: "Something went wrong" };
}
})Server Callbacks with Navigation
Server-level callbacks on .action() fire even when navigation occurs:
export const createPost = authActionClient
.inputSchema(schema)
.action(
async ({ parsedInput }) => {
const post = await db.post.create(parsedInput);
redirect(`/posts/${post.id}`);
},
{
onNavigation: async ({ navigationKind }) => {
// Runs on the server before the error is re-thrown
console.log("Server: navigation occurred:", navigationKind);
},
onSettled: async ({ result }) => {
// Also runs — good for cleanup/metrics
},
}
);Metadata
Note: Action files require a "use server" directive — omitted from examples below for brevity.What Is Metadata?
Metadata is typed data attached to each action, accessible in middleware and server callbacks. Common uses: action names for logging, feature flags, permission requirements.
Define a Metadata Schema
import { createSafeActionClient } from "next-safe-action";
import { z } from "zod";
export const actionClient = createSafeActionClient({
defineMetadataSchema: () =>
z.object({
actionName: z.string(),
}),
});When defineMetadataSchema is set, every action must call .metadata() before .action() — TypeScript enforces this.
Set Metadata Per Action
export const createUser = actionClient
.metadata({ actionName: "createUser" })
.inputSchema(z.object({ name: z.string() }))
.action(async ({ parsedInput, metadata }) => {
// metadata.actionName === "createUser"
return { name: parsedInput.name };
});Access Metadata in Middleware
export const actionClient = createSafeActionClient({
defineMetadataSchema: () =>
z.object({
actionName: z.string(),
requiresAuth: z.boolean().default(false),
}),
}).use(async ({ next, metadata }) => {
// metadata is fully typed: { actionName: string; requiresAuth: boolean }
if (metadata.requiresAuth) {
const session = await getSession();
if (!session) throw new Error("Unauthorized");
return next({ ctx: { userId: session.user.id } });
}
return next({ ctx: {} });
});// Public action
export const getPublicData = actionClient
.metadata({ actionName: "getPublicData", requiresAuth: false })
.action(async () => ({ data: "public" }));
// Protected action
export const getUserData = actionClient
.metadata({ actionName: "getUserData", requiresAuth: true })
.action(async ({ ctx }) => {
// ctx.userId is available because requiresAuth triggered the auth middleware
return await db.user.findById(ctx.userId);
});Metadata for Logging
const actionClient = createSafeActionClient({
defineMetadataSchema: () =>
z.object({ actionName: z.string() }),
}).use(async ({ next, metadata }) => {
const start = performance.now();
const result = await next({ ctx: {} });
const duration = performance.now() - start;
console.log(`[${metadata.actionName}] ${duration.toFixed(0)}ms`, {
success: !!result.data,
hasError: !!result.serverError,
});
return result;
});Metadata Validation Errors
If metadata doesn't match the schema, ActionMetadataValidationError is thrown at runtime. TypeScript catches most issues at compile time, but runtime validation is a safety net.
Rich Metadata Schemas
const actionClient = createSafeActionClient({
defineMetadataSchema: () =>
z.object({
actionName: z.string(),
category: z.enum(["user", "post", "admin", "system"]),
rateLimit: z.number().int().positive().optional(),
audit: z.boolean().default(true),
}),
});
export const deleteUser = actionClient
.metadata({
actionName: "deleteUser",
category: "admin",
rateLimit: 5,
audit: true,
})
.inputSchema(z.object({ userId: z.string().uuid() }))
.action(async ({ parsedInput }) => {
await db.user.delete(parsedInput.userId);
return { deleted: true };
});Type Inference Utilities
Available Types
All inference types are exported from next-safe-action:
import type {
InferSafeActionFnInput,
InferSafeActionFnResult,
InferCtx,
InferMetadata,
InferServerError,
InferMiddlewareFnNextCtx,
} from "next-safe-action";InferSafeActionFnInput
Infer the input types of an action function:
import type { InferSafeActionFnInput } from "next-safe-action";
const myAction = actionClient
.inputSchema(z.object({ name: z.string(), age: z.number() }))
.bindArgsSchemas([z.string().uuid()])
.action(async ({ parsedInput }) => parsedInput);
type Input = InferSafeActionFnInput<typeof myAction>;
// {
// clientInput: { name: string; age: number };
// bindArgsClientInputs: [string];
// parsedInput: { name: string; age: number };
// bindArgsParsedInputs: [string];
// }InferSafeActionFnResult
Infer the result type of an action function:
import type { InferSafeActionFnResult } from "next-safe-action";
type Result = InferSafeActionFnResult<typeof myAction>;
// SafeActionResult is a 4-branch discriminated union with mutually
// exclusive field presence (no explicit `status` field — the discriminant
// is which of `data` / `serverError` / `validationErrors` is populated):
//
// | { data?: undefined; serverError?: undefined; validationErrors?: undefined } // idle
// | { data: { name: string; age: number }; serverError?: undefined; validationErrors?: undefined }
// | { data?: undefined; serverError: string; validationErrors?: undefined }
// | { data?: undefined; serverError?: undefined; validationErrors: ValidationErrors<Schema> }
//
// Checking any one field (e.g. `if (result.data)`) narrows the other two to `undefined`.
// Runtime precedence when multiple outcomes coexist: validationErrors > serverError > data.InferCtx
Infer the context type from a client or middleware:
import type { InferCtx } from "next-safe-action";
const authClient = actionClient.use(async ({ next }) => {
return next({ ctx: { userId: "123", role: "admin" as const } });
});
type Ctx = InferCtx<typeof authClient>;
// { userId: string; role: "admin" }InferMetadata
Infer the metadata type from a client:
import type { InferMetadata } from "next-safe-action";
const client = createSafeActionClient({
defineMetadataSchema: () => z.object({ actionName: z.string() }),
});
type MD = InferMetadata<typeof client>;
// { actionName: string }InferServerError
Infer the server error type:
import type { InferServerError } from "next-safe-action";
const client = createSafeActionClient({
handleServerError: (e) => ({ message: e.message, code: "ERROR" as const }),
});
type SE = InferServerError<typeof client>;
// { message: string; code: "ERROR" }InferMiddlewareFnNextCtx
Infer the context a middleware passes to next():
import type { InferMiddlewareFnNextCtx } from "next-safe-action";
const authMiddleware = createMiddleware().define(async ({ next }) => {
return next({ ctx: { userId: "123" } });
});
type NextCtx = InferMiddlewareFnNextCtx<typeof authMiddleware>;
// { userId: string }Hook Return Type Inference
From next-safe-action/hooks:
import type {
InferUseActionHookReturn,
InferUseOptimisticActionHookReturn,
} from "next-safe-action/hooks";
type ActionReturn = InferUseActionHookReturn<typeof myAction>;
type OptimisticReturn = InferUseOptimisticActionHookReturn<typeof myAction, MyState>;From @next-safe-action/adapter-react-hook-form/hooks:
import type {
InferUseHookFormActionHookReturn,
InferUseHookFormOptimisticActionHookReturn,
} from "@next-safe-action/adapter-react-hook-form/hooks";
type HFReturn = InferUseHookFormActionHookReturn<typeof myAction, FormContext>;Practical Use: Typed Wrapper Components
import type { InferSafeActionFnResult } from "next-safe-action";
// Generic result display component
function ActionResult<T extends (...args: any[]) => any>({
result,
}: {
result: InferSafeActionFnResult<T>;
}) {
if (result.serverError) return <div className="error">{result.serverError}</div>;
if (result.data) return <div className="success">Success!</div>;
return null;
}Related skills
How it compares
Use safe-action-advanced after basic next-safe-action setup; pick generic Next.js skills when not using the next-safe-action library.
FAQ
What framework errors does safe-action-advanced cover?
safe-action-advanced documents handling redirect, notFound, forbidden, and unauthorized errors inside next-safe-action server actions so Next.js navigation and auth flows work without breaking action contracts.
What are bind arguments in next-safe-action?
safe-action-advanced explains passing extra arguments to server actions through .bind(), such as resource IDs, while preserving next-safe-action validation and type inference on the bound action function.