Install
$ agentstack add skill-cors-gmbh-pimcore-skills-pimcore-studio ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo 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 Used
- ✓ Filesystem access No
- ✓ 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.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
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 →About
Pimcore Studio v2 Development
You are helping build features for Pimcore Studio v2 (React/TypeScript). Before writing code, load context about the architecture and patterns.
Tech Stack
Pimcore Studio v2 is built with:
- React 18 — UI components
- TypeScript — Type safety
- Ant Design (antd) — UI component library (Form, Table, Modal, Select, Input, Button, Tabs, Tree, etc.)
- InversifyJS — Dependency injection container
- react-i18next — Internationalization (
useTranslation()hook) - Rsbuild — Build tool with Module Federation for plugin loading
Discovering Pimcore Studio Source
When unsure how to do something, read the Studio source code:
vendor/pimcore/studio-ui-bundle/assets/js/src/
├── core/
│ ├── app/
│ │ ├── depency-injection/ # DI container setup (InversifyJS)
│ │ ├── module-system/ # Module registration system
│ │ ├── plugin-system/ # Plugin lifecycle (IAbstractPlugin)
│ │ ├── config/services/ # Service IDs (serviceIds constant)
│ │ ├── i18n/ # Translation setup (react-i18next)
│ │ ├── api/ # API utilities
│ │ └── public-api/ # Public API gateway, element API
│ ├── components/ # Pimcore UI components (built on antd)
│ │ ├── form/ # Form controls
│ │ ├── sidebar/ # Sidebar components
│ │ ├── split/ # Split layouts
│ │ └── ...
│ └── utils/ # Utility hooks and helpers
│ ├── hooks/ # useDebounce, useClickOutside, etc.
│ └── ...
└── modules/ # Pimcore feature modules
├── element/ # Element handling, DynamicType registries
├── widget-manager/ # Widget registry
└── ...
Search for examples:
grep -r "registerWidget\|registerDynamicType\|IAbstractPlugin" \
vendor/pimcore/studio-ui-bundle/assets/js/src/
Step 1: Plugin Architecture
Plugin Lifecycle
import { type IAbstractPlugin, container } from '@pimcore/studio-ui-bundle'
const plugin: IAbstractPlugin = {
name: 'my-plugin',
onInit() {
// Phase 1: Runs first, before any modules
// - Container bindings (InversifyJS)
// - Dynamic type registration
// - Registry creation
},
onStartup({ moduleSystem }) {
// Phase 2: Runs after ALL plugins' onInit()
// - Module registration
// - Widget registration
// - Can safely use container bindings from other plugins
}
}
export default plugin
Order matters: All onInit() calls complete before any onStartup(). Bindings must happen in onInit().
Module System
Modules encapsulate feature registrations:
import { type AbstractModule, container } from '@pimcore/studio-ui-bundle'
export const MyModule: AbstractModule = {
onInit(): void {
// Register services, form builders, extensions
}
}
// In plugin's onStartup():
moduleSystem.registerModule(MyModule)
DI Container (InversifyJS)
import { container } from '@pimcore/studio-ui-bundle'
// Bind
container.bind('MyServiceId').to(MyClass).inSingletonScope()
container.bind('MyServiceId').toConstantValue(myInstance)
// Retrieve
const service = container.get('MyServiceId')
Step 2: Core Studio v2 Services
Widget Registry
import { serviceIds } from '@pimcore/studio-ui-bundle/app'
import { type WidgetRegistry } from '@pimcore/studio-ui-bundle/modules/widget-manager'
const widgetRegistry = container.get(serviceIds['WidgetRegistry'])
widgetRegistry.registerWidget({ name: 'my.widget', component: MyComponent })
Dynamic Type Registry
import { DynamicTypeObjectDataRegistry } from '@pimcore/studio-ui-bundle/modules/element'
const registry = container.get(
serviceIds['DynamicTypes/ObjectDataRegistry']
)
registry.registerDynamicType(new MyDynamicType())
Step 3: Dynamic Types (Pimcore Data Object Fields)
For custom field types in the class definition editor:
import {
DynamicTypeObjectDataAbstractSelect,
DynamicTypeObjectDataAbstractMultiSelect,
DynamicTypeObjectDataAbstract,
DynamicTypeFieldFilterMultiselect,
} from '@pimcore/studio-ui-bundle/modules/element'
// Single select
export class DynamicTypeMyField extends DynamicTypeObjectDataAbstractSelect {
readonly id = 'myFieldType' // MUST match PHP CoreExtension $fieldtype
readonly dynamicTypeFieldFilterType = new DynamicTypeFieldFilterMultiselect()
}
// Multi-select
export class DynamicTypeMyFieldMulti extends DynamicTypeObjectDataAbstractMultiSelect {
readonly id = 'myFieldTypeMultiselect'
readonly dynamicTypeFieldFilterType = new DynamicTypeFieldFilterMultiselect()
}
// Custom rendering
export class DynamicTypeMyCustom extends DynamicTypeObjectDataAbstract {
readonly id = 'myCustomFieldType'
getObjectDataComponent(props: any): React.ReactElement {
return
}
}
Register in onInit() — never in onStartup().
Step 4: Building Components with Ant Design
Form Component
import React from 'react'
import { Form, Input, Switch, InputNumber, Select } from 'antd'
import { useTranslation } from 'react-i18next'
interface MyFormProps {
data: MyEntity
onChange: (data: MyEntity) => void
}
export const MyForm: React.FC = ({ data, onChange }) => {
const { t } = useTranslation()
return (
onChange({ ...data, name: e.target.value })} />
onChange({ ...data, active: checked })} />
)
}
Table/List Component
import React, { useEffect, useState } from 'react'
import { Table, Button, Space, Popconfirm } from 'antd'
export const MyList: React.FC = () => {
const [data, setData] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
void (async () => {
try { setData(await myApi.list()) }
finally { setLoading(false) }
})()
}, [])
return (
v ? 'Yes' : 'No' },
{ title: 'Actions', render: (_, record) => (
handleDelete(record.id)}>
Delete
)},
]} />
)
}
Modal / Dialog
import { Modal, Form, Input } from 'antd'
const [open, setOpen] = useState(false)
setOpen(false)}>
Step 5: Select Components with Module-Level Caching
ALWAYS use this pattern for any Select that loads options from an API:
import React from 'react'
import { Select, type SelectProps } from 'antd'
let cachedOptions: Array | null = null
let loadPromise: Promise | null = null
const loadOptions = async () => {
if (cachedOptions) return cachedOptions
if (loadPromise) return loadPromise
loadPromise = (async () => {
try {
const items = await myApi.list()
cachedOptions = items.map(i => ({ value: i.id, label: i.name }))
return cachedOptions
} finally { loadPromise = null }
})()
return loadPromise
}
export const clearCache = () => { cachedOptions = null; loadPromise = null }
export const MySelect: React.FC = (props) => {
const [options, setOptions] = React.useState(cachedOptions || [])
const [loading, setLoading] = React.useState(!cachedOptions)
React.useEffect(() => {
void (async () => {
if (!cachedOptions) setLoading(true)
try { setOptions(await loadOptions()) }
finally { setLoading(false) }
})()
}, [])
return
(opt?.label ?? '').toLowerCase().includes(input.toLowerCase())
} />
}
Step 6: API Layer
export interface MyEntity {
id: number
name: string
active: boolean
}
export const myApi = {
async list(): Promise {
const res = await fetch('/pimcore-studio/api/my-bundle/entities')
return (await res.json()).data ?? (await res.json())
},
async get(id: number): Promise {
return (await fetch(`/pimcore-studio/api/my-bundle/entities/${id}`)).json()
},
async save(entity: Partial): Promise {
const method = entity.id ? 'PUT' : 'POST'
const url = entity.id
? `/pimcore-studio/api/my-bundle/entities/${entity.id}`
: '/pimcore-studio/api/my-bundle/entities'
return (await fetch(url, {
method, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(entity)
})).json()
},
async delete(id: number): Promise {
await fetch(`/pimcore-studio/api/my-bundle/entities/${id}`, { method: 'DELETE' })
},
}
Step 7: Translations
File: Resources/translations/studio.en.yaml
my_entity: 'My Entity'
my_entity_name: 'Name'
Usage:
import { useTranslation } from 'react-i18next'
const { t } = useTranslation()
t('my_entity_name')
Step 8: Build System
- Rsbuild with Module Federation for plugin loading
npm run build— Production buildnpm run dev— Development with hot reload
package.json Template
{
"name": "@my-scope/my-plugin",
"version": "1.0.0",
"main": "src/index.ts",
"dependencies": {
"@pimcore/studio-ui-bundle": "^1.0.0",
"antd": "^5.12.0",
"react": "18.3.x",
"react-i18next": "^14.0.0"
}
}
Step 9: Directory Structure
Resources/assets/pimcore-studio/
├── package.json
└── src/
├── main.ts # Plugin entry (IAbstractPlugin)
├── modules/
│ ├── icon-library/ # Icon registrations
│ └── {feature}/
│ ├── api.ts # API layer
│ ├── MyComponent.tsx # React components (using antd)
│ └── index.ts
├── components/ # Shared components
│ └── MySelect.tsx # Selects with caching
└── dynamic-types/ # Pimcore field types
└── DynamicTypeMyField.tsx
Reference
See .claude/skills/pimcore-studio/patterns.md for complete code templates.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: cors-gmbh
- Source: cors-gmbh/pimcore-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.