AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Zenstack Plugin Dev

skill-zenstackhq-skills-zenstack-plugin-dev · by zenstackhq

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).

No reviews yet
0 installs
26 views
0.0% view→install

Install

$ agentstack add skill-zenstackhq-skills-zenstack-plugin-dev

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access Used
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-zenstackhq-skills-zenstack-plugin-dev)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
2mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Zenstack Plugin Dev? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

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 {
    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 = 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

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.