Skip to content
Content
Skill

/new-entry-wizard

Building a New Entry Wizard for the Headless CMS. Use this skill when the developer wants to show a custom wizard UI before the entry form when creating new CMS entries -- collecting fields like title, slug, or category upfront, then pre-filling the entry form. Covers

BOOST
From plugin
webiny-js
8k76 skills3 MCP
Install
$ npx -y skills add webiny/webiny-js --skill new-entry-wizard --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/new-entry-wizard

Context preview

The summary Claude sees to decide when to auto-load this skill.

Building a New Entry Wizard for the Headless CMS. Use this skill when the developer wants to show a custom wizard UI before the entry form when creating new CMS entries -- collecting fields like title, slug, or category upfront, then pre-filling the entry form. Covers

SKILL.md

new-entry-wizard.SKILL.md
name: webiny-new-entry-wizard
description: >
  Building a New Entry Wizard for the Headless CMS. Use this skill when the developer wants to
  show a custom wizard UI before the entry form when creating new CMS entries -- collecting
  fields like title, slug, or category upfront, then pre-filling the entry form. Covers
  ContentEntryEditorConfig.NewEntryWizard, createFeature, FormModelFactory, createReactiveComponent,
  useContentEntryFormPresenter, and the dirty-flag on setData.

New Entry Wizard

TL;DR

A New Entry Wizard intercepts the "create new entry" flow in the Headless CMS and shows a custom form before the full entry editor. The wizard collects initial values (title, slug, category, etc.), then calls `formPresenter.newEntry(initialValues)` to pre-fill the entry form and transition to the editor. The form is automatically marked as dirty so the user sees unsaved changes.

A wizard extension has five files:

1. **`abstractions.ts`** -- DI token and presenter interface (ALWAYS a standalone file) 2. **`feature.ts`** -- `createFeature` wiring (ALWAYS a standalone file) 3. **Presenter** -- owns the wizard form (fields, layout, validation) via `FormModelFactory` 4. **View** -- renders the wizard UI using `FormView` and `FormErrors` 5. **Registration** -- plugs the wizard into specific content models via `ContentEntryEditorConfig.NewEntryWizard`

Registration

// extensions/myWizard/index.tsx
import React from "react";
import { RegisterFeature } from "webiny/admin";
import { ContentEntryEditorConfig } from "webiny/admin/cms/entry/editor";
import { MyWizardFeature } from "./feature.js";
import { MyWizardForm } from "./MyWizardForm.js";

const MyWizardExtension = () => {
  return (
    <>
      <RegisterFeature feature={MyWizardFeature} />
      <ContentEntryEditorConfig>
        <ContentEntryEditorConfig.NewEntryWizard
          element={<MyWizardForm />}
          modelIds={["article", "blogPost"]}
        />
      </ContentEntryEditorConfig>
    </>
  );
};

export default MyWizardExtension;

Props

| Prop | Type | Description | | ---------- | -------------------- | ----------------------------------------------------------------------------------- | | `element` | `React.ReactElement` | The wizard component to render | | `modelIds` | `string[]` | Content model IDs this wizard applies to. Omit or pass `[]` to apply to all models. |

Enabling the extension

// webiny.config.tsx
<Admin.Extension src={"@/extensions/myWizard/index.tsx"} />

Abstractions

Abstractions MUST live in a dedicated `abstractions.ts` file. This file defines the DI token for the presenter. It is imported by both the feature and the view.

// extensions/myWizard/abstractions.ts
import { createAbstraction } from "webiny/admin";
import type { FormModel } from "webiny/admin/form";

export interface MyWizardVM {
  form: FormModel.FormVM;
  data: Record<string, unknown>;
}

export interface IMyWizardPresenter {
  vm: MyWizardVM;
  submit(): Promise<Record<string, unknown> | false>;
  reset(): void;
}

export const MyWizardPresenter = createAbstraction<IMyWizardPresenter>("MyWizardPresenter");

Feature

The feature MUST live in a dedicated `feature.ts` file. It wires the presenter implementation to the abstraction and defines how the feature is resolved from the DI container.

// extensions/myWizard/feature.ts
import { createFeature, FormModelFactory } from "webiny/admin";
import { MyWizardPresenter } from "./abstractions.js";
import { MyWizardPresenterImpl } from "./MyWizardPresenter.js";

export const MyWizardFeature = createFeature({
  name: "MyWizard",
  register(container) {
    container.register(
      MyWizardPresenter.createImplementation({
        implementation: MyWizardPresenterImpl,
        dependencies: [FormModelFactory]
      })
    );
  },
  resolve(container) {
    return { presenter: container.resolve(MyWizardPresenter) };
  }
});

Presenter

The presenter owns a `FormModel` for the wizard's own fields. It is a plain class -- the DI token lives in `abstractions.ts` and the feature wiring lives in `feature.ts`.

// extensions/myWizard/MyWizardPresenter.ts
import type { FormModel } from "webiny/admin/form";
import { makeAutoObservable, toJS } from "mobx";
import { FormModelFactory } from "webiny/admin";
import type { IMyWizardPresenter, MyWizardVM } from "./abstractions.js";

export class MyWizardPresenterImpl implements IMyWizardPresenter {
  private form: FormModel.Interface;

  constructor(formFactory: FormModelFactory.Interface) {
    this.form = formFactory.create({
      fields: fields => ({
        title: fields.text().label("Title").required("Title is required"),
        slug: fields.text().label("Slug").required("Slug is required"),
        category: fields
          .text()
          .label("Category")
          .options([
            { label: "News", value: "news" },
            { label: "Tutorial", value: "tutorial" }
          ])
      }),
      layout: layout => [layout.row("title"), layout.row("slug"), layout.row("category")]
    });

    makeAutoObservable(this);
  }

  get vm(): MyWizardVM {
    return {
      form: this.form.vm,
      data: toJS(this.form.getData())
    };
  }

  async submit(): Promise<Record<string, unknown> | false> {
    return this.form.submit();
  }

  reset(): void {
    this.form.reset();
  }
}

The presenter uses the same `FormModel` system as all Webiny forms. See the `webiny-form-model` skill for field types, renderers, layout options, and validation.

View

The view renders the wizard form and handles submission. On submit, it calls `formPresenter.newEntry(initialValues)` to transition from the wizard to the entry editor with pre-filled values.

// extensions/myWizard/MyWizardForm.tsx
imp
Read more
Ships withwebiny-js

Open-source content platform. Self-hosted on AWS serverless. Built as a TypeScript framework you extend with code, not a closed product you configure through a UI. Runs on Lambda, DynamoDB, S3, and CloudFront inside your own AWS account. Scales automatically.

Get the whole plugin
Stats
8,049
Stars
682
Forks
Active
Maintenance
TypeScript
Language
1d ago
Last commit
8y ago
Created
3d ago
Added

Repo: webiny/webiny-js

Other skills on webiny-js.