zenstackhq/skills

zenstack-plugin-dev

Develop ZenStack V3 plugins. Use when building or declaring a plugin — custom ZModel attributes/functions, CLI code generators (CliPlugin with a generate callback for `zen generate`), or runtime plugins installed via $use/definePlugin (onQuery, onKyselyQuery, onEntityMutation hooks, extending the client, custom query args, computed result fields).

Trending #7594 First seen Jun 7, 2026

Installation

$ npx skills add zenstackhq/skills --skill zenstack-plugin-dev

Summary

  • Develop ZenStack V3 plugins.
  • Use when building or declaring a plugin — custom ZModel attributes/functions, CLI code generators (CliPlugin with a generate callback for `zen generate`), or runtime plugins installed via $use/definePlugin (onQuery, onKyselyQuery, onEntityMutation hooks, extending the client, custom query args, computed result fields).

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from zenstackhq/skills.

npx skills add zenstackhq/skills

Browse all from zenstackhq/skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 3
License LICENSE
Default branch main
Open issues 0
Status Active

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 9,895 B
  • docs SUMMARY.md 378 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 112 installs

SKILL.md

ZenStack V3 — Plugin Development

Plugins are a preview feature and may change in future releases.

A ZenStack plugin can extend the toolkit at three levels, independently or together:

  1. Schema (ZModel) — contribute custom attributes (e.g. @password) and functions to the

schema language.

  1. CLI (code generation) — participate in zen generate to emit artifacts from the schema.
  2. Runtime (ORM) — install on the client to intercept queries, hook CRUD lifecycle events,

transform SQL, add custom methods, or compute fields.

ZenStack's plugin replaces Prisma's generator. To declare/consume plugins in a schema see zenstack-schema-modeling; this skill is about authoring them.

Packages: @zenstackhq/orm (runtime plugin types), @zenstackhq/sdk (CLI plugin types + ZModel AST). Install the SDK as a dev dependency: npm install -D @zenstackhq/sdk.

Declaring a plugin in ZModel

plugin myPlugin {
    provider = './my-plugin'   // local .ts/.js/.mts/.mjs file, a folder with an index, or an npm package
    output   = './generated'   // arbitrary key=value options, readable by the plugin
}

provider resolution order: local file path → folder with an index → npm package. CLI plugins must be ESM; TypeScript providers are loaded via jiti. The built-in @core/typescript plugin runs automatically; @core/prisma emits a Prisma schema.

Runtime plugins

A runtime plugin is an object implementing RuntimePlugin from @zenstackhq/orm. Define it three ways; definePlugin gives the best type inference:

import { definePlugin, type RuntimePlugin } from '@zenstackhq/orm';

// plain object / definePlugin (recommended)
const plugin = definePlugin({ id: 'my-plugin', /* hooks… */ });

// or a class
class MyPlugin implements RuntimePlugin<typeof schema> {
    id = 'my-plugin';
    /* hooks… */
}

Install on the client (immutable — returns a new client, original unchanged); remove with $unuse(id) / $unuseAll(). You can also pass plugins: [...] to the ZenStackClient constructor.

const db = new ZenStackClient(schema, { dialect });
const withPlugin = db.$use(plugin);              // new client
const plain = withPlugin.$unuse('my-plugin');    // remove by id
// or: new ZenStackClient(schema, { dialect, plugins: [plugin] });

Every plugin needs a unique id. Add any combination of the hooks below.

onQuery — intercept ORM operations

Fires for ORM API calls only (findMany, create, …), not the query builder. Wrap, modify args, short-circuit, or post-process results via proceed.

definePlugin({
    id: 'timing',
    onQuery: async ({ model, operation, args, proceed }) => {
        const start = Date.now();
        const result = await proceed(args);
        console.log(`${model}.${operation} took ${Date.now() - start}ms`);
        return result;
    },
});

onKyselyQuery — intercept/transform SQL

The lowest-level hook: every DB access (ORM and query builder) flows through Kysely here, so you can inspect or rewrite the SQL AST (OperationNode) before proceed. Good for soft-deletes, injected filters, column transforms.

import type { OnKyselyQueryCallback } from '@zenstackhq/orm';

const hook: OnKyselyQueryCallback<typeof schema> = async ({ query, proceed }) => {
    // inspect/transform `query` (a Kysely OperationNode), e.g. with an OperationNodeTransformer
    return proceed(query);
};
definePlugin({ id: 'sql-rewrite', onKyselyQuery: hook });

onEntityMutation — before/after create·update·delete

Lifecycle hooks around entity mutations, with lazy loaders for the affected rows.

definePlugin({
    id: 'audit',
    onEntityMutation: {
        runAfterMutationWithinTransaction: false, // default: run after the tx commits
        async beforeEntityMutation({ model, action, loadBeforeMutationEntities }) {
            // action: 'create' | 'update' | 'delete'
            if (model === 'Post' && action === 'update') {
                const before = await loadBeforeMutationEntities();
            }
        },
        async afterEntityMutation({ model, action, loadAfterMutationEntities }) {
            const after = await loadAfterMutationEntities();
        },
    },
});

Set runAfterMutationWithinTransaction: true to run the after-hook atomically inside the mutation's transaction. Note: mutations triggered by DB cascades are not captured, and the loaders can be expensive on bulk mutations.

Extending the client (client)

Add custom methods/properties under client. Names must be $-prefixed so they don't shadow model accessors; grouping under one $namespace object is recommended.

const cacheDb = db.$use(definePlugin({
    id: 'cache',
    client: {
        $cache: {
            get stats() { return { hits: 0, misses: 0 }; },
            async invalidate() { /* … */ },
        },
    },
}));
await cacheDb.$cache.invalidate();

Custom query args (queryArgs)

Accept extra options on ORM calls, validated with a Zod schema. Target $all, the $create/$read/$update/$delete groups, or specific operations.

import { z } from 'zod';

definePlugin({
    id: 'cache',
    queryArgs: {
        $read: z.object({ cache: z.strictObject({ ttl: z.number().positive().optional() }).optional() }),
    },
    onQuery: ({ args, proceed }) => proceed(args), // read (args as any).cache
});
// await db.user.findMany({ where: {...}, cache: { ttl: 60 } });

Computed result fields (result, v3.5.0+)

Add virtual fields computed after a query from declared needs fields.

definePlugin({
    id: 'full-name',
    result: {
        user: {
            fullName: {
                needs: { firstName: true, lastName: true },
                compute: (user) => `${user.firstName} ${user.lastName}`,
            },
        },
    },
});

CLI plugins (code generation)

A CLI plugin is an ESM module whose default export is a CliPlugin with a generate callback. It runs during zen generate, receiving the parsed ZModel AST.

import type { CliPlugin } from '@zenstackhq/sdk';
import { isDataModel, type InvocationExpr } from '@zenstackhq/sdk/ast';
import fs from 'node:fs';

const plugin: CliPlugin = {
    name: 'Password Report',
    // statusText is optional
    generate: ({ model, defaultOutputPath, pluginOptions }) => {
        if (pluginOptions['report'] !== true) return;       // options come from the ZModel plugin block
        let out = '# Password Fields\n\n';
        for (const dm of model.declarations.filter(isDataModel)) {
            for (const field of dm.fields) {
                const attr = field.attributes.find((a) => a.decl.$refText === '@password');
                if (attr) out += `- ${dm.name}.${field.name}\n`;
            }
        }
        fs.writeFileSync(`${defaultOutputPath}/password-fields.md`, out, 'utf-8');
    },
};
export default plugin;

generate receives { model, defaultOutputPath, pluginOptions } — the ZModel AST, the resolved output directory (the output option or a default), and the options from the plugin block.

Schema plugins (custom attributes & functions)

Declare new ZModel attributes/functions in a .zmodel the plugin ships, then use them in the app schema. @@@targetField constrains where an attribute applies.

// my-plugin/plugin.zmodel
attribute @password(hasher: Any) @@@targetField([StringField])
function bcryptHasher(rounds: Int): Any {}
// app schema
model User {
    id       Int    @id @default(autoincrement())
    password String @password(hasher: bcryptHasher(10))
}

End-to-end pattern: password hasher

The docs' worked example combines all three levels into one plugin package:

  1. Schemaplugin.zmodel defines @password + bcryptHasher(); the app schema applies

@password(hasher: bcryptHasher(10)) to a field.

  1. CLI — a CliPlugin.generate walks the AST and emits a "password fields" report when the

report option is set.

  1. Runtime — a RuntimePlugin with onKyselyQuery uses a Kysely OperationNodeTransformer to

detect @password fields (via schema.models[...].fields[...].attributes) and hash values on INSERT/UPDATE before they hit the database.

Install with db.$use(new PasswordHasherPlugin()) (or the plugins constructor option), then npm install -D @zenstackhq/sdk and zen generate to run the CLI side.

Use Zod v4 for queryArgs schemas to match ZenStack's bundled version.

Reference docs

Full ZenStack documentation for this topic is bundled under [references/](references/):

  • [plugin-dev.md](references/plugin-dev.md) — end-to-end plugin development guide
  • [runtime-plugins-overview.md](references/runtime-plugins-overview.md) — runtime plugin model
  • [extending-orm-client.md](references/extending-orm-client.md) — client methods, query args, computed fields
  • [entity-mutation-hooks.md](references/entity-mutation-hooks.md) — before/after mutation hooks
  • [kysely-query-hooks.md](references/kysely-query-hooks.md) — onKyselyQuery SQL transformation
  • [query-api-hooks.md](references/query-api-hooks.md) — onQuery ORM interception
  • [zmodel-plugin.md](references/zmodel-plugin.md) — declaring plugins in ZModel
  • [zmodel-plugin-reference.md](references/zmodel-plugin-reference.md) — plugin block reference