#Complete example
This guide walks you through building a complete blog platform schema using the Management SDK. You'll create models, components, relations, enumerations, and configure conditional visibility.
#What you'll build
A blog platform with:
Postmodel with title, content, status, and a featured flagAuthormodel with name, bio, and emailCategorymodel with name and descriptionSeoMetadatacomponent (reusable SEO fields)AuthorInfocomponent (social media info)CallToActionandImageBlockcomponents (content blocks)- Post-to-Author relation (many-to-one)
- Post-to-Category relation (many-to-many)
- Component embeddings (SEO in Post, social info in
Author) - Modular component field (flexible content blocks)
- Nested components (ContactDetails nested inside
AuthorInfo) - Status enumeration (
DRAFT,REVIEW,PUBLISHED) - Conditional visibility rules
#Prerequisites
Before you begin, ensure that you have the following:
- Hygraph project created
- Permanent Auth Token with Management API permissions
- Node.js 18 or later installed
- Management SDK installed:
npm install @hygraph/management-sdk - Content API endpoint from Project Settings > Endpoints > High Performance Content API
#Dependency order
The Management SDK executes operations sequentially. You need to create dependencies before referencing them.
For example:
- Creating a relation before its target model exists will fail.
- Creating an enumerable field before its enumeration exists will fail.
- Embedding a component before it exists will fail.
- Nesting a component before the parent component exists will fail.
Create models (Author, Category, Post), and add simple fields to them↓Create enumerations (PostStatus)↓Add enumerable field (status) to Post↓Create components (SeoMetadata, AuthorInfo, CallToAction, ImageBlock), and add simple fields to them↓Embed components in models (SeoMetadata in Post, AuthorInfo in Author)↓Create nested components (ContactDetails nested inside AuthorInfo)↓Create modular component field (contentBlocks in Post)↓Create relations (Post→Author, Post→Category)↓Conditional visibility rules
#Initialize the client
Create the create-blog-schema.ts file:
import { Client } from '@hygraph/management-sdk';const client = new Client({authToken: '',endpoint: '',name: 'create-blog-schema-v1', // Unique migration name});async function createBlogSchema() {try {// All operations will go here} catch (error) {console.error('Migration failed:', error);process.exit(1);}}createBlogSchema();
#Create models
Models must exist before you can add fields to them. The values of apiId and apiIdPlural must be different.
#Add fields to models
Now, you can add fields to your models. Note the following points:
- Only one field per model can be
isTitle: true. This field appears as the entry identifier in the UI. initialValuesets the default for new entries.- For boolean fields, use
trueorfalse.
#Create an enumeration
Create an enumeration before configuring fields that reference them.
// Create PostStatus enumerationclient.createEnumeration({apiId: 'PostStatus',displayName: 'Post Status',description: 'Workflow status for blog posts',values: [{ apiId: 'DRAFT', displayName: 'Draft' },{ apiId: 'REVIEW', displayName: 'In Review' },{ apiId: 'PUBLISHED', displayName: 'Published' },],});console.log('Created PostStatus enumeration');
#Add enumeration to model
The enumeration, PostStatus, must already exist. The initialValue must match one of the enumeration's values.
// Post: status (enumerable field)client.createEnumerableField({parentApiId: 'Post',apiId: 'status',displayName: 'Status',enumerationApiId: 'PostStatus',isRequired: true,initialValue: 'DRAFT',description: 'Publication status',});console.log('Added status enumeration to Post');
#Create components
Components are reusable field groups that can be embedded in multiple models. Create components before embedding them in models.
#Embed component in model
Now let's embed the SEO component into the Post model. You can add SEO metadata to each post without duplicating fields across models.
// Post: Embed SeoMetadata componentclient.createComponentField({parentApiId: 'Post',apiId: 'seo',displayName: 'SEO',componentApiId: 'SeoMetadata',isRequired: false,isList: false,description: 'SEO metadata for search engines',});console.log('Embedded SEO component in Post');
#Create modular component field
Modular components allow editors to choose from multiple component types, perfect for page builders. Editors can now add multiple CallToAction and ImageBlock components in any order within a post. A post might have:
ImageBlock: A hero image or an infographicCallToAction: Subscribe prompt or download guide
// Post: Content blocks (modular component)client.createComponentUnionField({parentApiId: 'Post',apiId: 'contentBlocks',displayName: 'Content Blocks',componentApiIds: ['CallToAction', 'ImageBlock'],isList: true,isRequired: false,description: 'Flexible content blocks for rich posts',});console.log('Created modular component field');
#Create nested components
Components can be nested inside other components for deeply hierarchical structures. Let's create a nested Address component. In this example, notice parentApiId: 'AuthorInfo'. We're nesting a component inside another component, not inside a model. This creates a two-level hierarchy: Author → AuthorInfo → ContactDetails.
// Create child componentclient.createComponent({apiId: 'ContactDetails',apiIdPlural: 'ContactDetailsCollection',displayName: 'Contact Details',description: 'Phone and email contact information',});// Add fields to the componentclient.createSimpleField({parentApiId: 'ContactDetails',apiId: 'phone',displayName: 'Phone',type: SimpleFieldType.STRING,isRequired: false,description: 'Contact phone number',visibility: VisibilityTypes.READ_WRITE});client.createSimpleField({parentApiId: 'ContactDetails',apiId: 'email',displayName: 'Email',type: SimpleFieldType.STRING,isRequired: true, // Email is requireddescription: 'Contact email address',validations: {String: {matches: {regex: '^([a-z0-9_\\.\\+-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$',errorMessage: 'Please enter a valid email address'}}}});console.log('Created ContactDetails component');// Nest ContactDetails inside AuthorInfo componentclient.createComponentField({parentApiId: 'AuthorInfo',apiId: 'contact',displayName: 'Contact',componentApiId: 'ContactDetails',isRequired: false,isList: false, // Single contact (or true for multiple)description: 'Contact information',visibility: VisibilityTypes.READ_WRITE});console.log('Nested ContactDetails inside AuthorInfo');
Result structure:
Author (model)└── AuthorInfo (component)├── website (STRING field)├── twitter (STRING field)├── location (STRING field)└── contact (component field) ← NESTED└── ContactDetails (component)├── phone (STRING field)└── email (STRING field)
#Create a many-to-one reference
One author can write multiple posts. For this use case, we need to create a many-to-one relation.
client.createRelationalField({parentApiId: 'Post',apiId: 'author',displayName: 'Author',type: RelationalFieldType.RELATION, // Use enum, not stringreverseField: {apiId: 'posts',modelApiId: 'Author', // Required! Specify the related modeldisplayName: 'Posts',isList: true, // Required! Author has many postsvisibility: VisibilityTypes.READ_WRITE // Use visibility instead of isHidden},isList: false, // Post has one authorisRequired: false, // Cannot be required for RELATION type (only ASSET)description: 'Post author'});console.log('Created Post→Author relation');
#Create a many-to-many reference
A post can belong to multiple categories. A category can have multiple posts. For this use case, we need to create a many-to-many relation.
client.createRelationalField({parentApiId: 'Post',apiId: 'categories',displayName: 'Categories',type: RelationalFieldType.RELATION, // Use enum, not stringreverseField: {apiId: 'posts',modelApiId: 'Category', // Required! Specify the related model heredisplayName: 'Posts',isList: true, // Required! Category has many postsvisibility: VisibilityTypes.READ_WRITE // Use visibility instead of isHidden},isList: true,isRequired: false,description: 'Post categories'});console.log('Created Post→Category relation');
#Add conditional visibility
Make the excerpt field required only when status is PUBLISHED. Otherwise, it remains optional.
client.updateSimpleField({apiId: "excerpt",parentApiId: "Post",isRequired: true,visibility: VisibilityTypes.READ_WRITE,visibilityCondition: {baseField: "status", // API ID of the enumerable field (not the enum name)operator: FieldConditionOperator.IS,enumerationValues: ["PUBLISHED"],booleanValue: null}});console.log('Configured conditional visibility on excerpt');
#Advanced features
#Add a taxonomy
Taxonomies organize content into hierarchical categories. Let's add a category taxonomy and add the taxonomy to the Post model. Posts can now be organized using a hierarchical category tree.
// Create the taxonomy with all nodes at onceclient.createTaxonomy({apiId: 'BlogCategories',displayName: 'Blog Categories',description: 'Hierarchical blog categorization',taxonomyNodes: [// Top-level categories (parent: null){ apiId: 'technology', displayName: 'Technology', parent: null },{ apiId: 'business', displayName: 'Business', parent: null },// Subcategories{ apiId: 'webDev', displayName: 'Web Development', parent: 'technology' },{ apiId: 'aiMl', displayName: 'AI & Machine Learning', parent: 'technology' }]});// Add taxonomy field to Post modelclient.createTaxonomyField({parentApiId: 'Post',apiId: 'taxonomyCategories',displayName: 'Taxonomy Categories',taxonomyApiId: 'BlogCategories',isList: true,isRequired: false,description: 'Hierarchical categorization'});console.log('Created taxonomy and taxonomy field');
#Add a workflow
Create an editorial workflow for content approval. Posts can move through the draft → review → approved stages before publication.
client.createWorkflow({apiId: 'editorialWorkflow',displayName: 'Editorial Workflow',description: 'Content review and approval process',enabled: true, // Required!steps: [ // Not "stages"!{apiId: 'draft',displayName: 'Draft',description: 'Work in progress',color: ColorPalette.BLUE, // Use enum, not stringallowEdit: true, // Required!allowedRoles: ['role-id-1'], // Required! Array of role IDsposition: 0},{apiId: 'review',displayName: 'In Review',description: 'Awaiting editorial review',color: ColorPalette.YELLOW, // Use enumallowEdit: false, // Required!returnToStep: 'draft', // Can return to draft if rejectedallowedRoles: ['role-id-2'], // Required!position: 1},{apiId: 'approved',displayName: 'Approved',description: 'Ready for publication',color: ColorPalette.GREEN, // Use enumallowEdit: false, // Required!allowedRoles: ['role-id-3'], // Required!publishStages: ['published'], // Optional: stages that can be published from this stepposition: 2},],});console.log('Created editorial workflow');
#Add a webhook
Notify external systems when posts are published. Marketing system receives notifications when posts are published.
client.createWebhook({name: 'Post Publish Notification',description: 'Notify marketing system when blog posts are published',url: 'https://api.marketing.example.com/webhooks/blog-published',method: WebhookMethod.POST,headers: {Authorization: 'Bearer webhook-secret-token','Content-Type': 'application/json',},isActive: true,includePayload: true,models: [], // Empty array = all models (including future ones)stages: [], // Empty array = all stages (including future ones)triggerType: WebhookTriggerType.CONTENT_MODEL,triggerActions: [WebhookTriggerAction.PUBLISH],secretKey: 'webhook-secret-key'});
#Test with dry run
Before applying changes to production, test the migration:
// At the start of createBlogSchema(), before try block:const changes = client.dryRun();console.log('=== DRY RUN RESULTS ===');console.log(`Operations: ${changes.length}`);console.log(JSON.stringify(changes, null, 2));console.log('======================');// Exit without runningprocess.exit(0);
Review the output to ensure all operations are correct. Then remove the dry run code and proceed to production.
#Run the migration
async function createBlogSchema() {try {// ... all operations here ...// Execute migrationconst result = await client.run(true);if (result.errors) {console.error('Migration failed with errors:', result.errors);process.exit(1);}console.log('Migration completed successfully');console.log(`Migration name: ${result.name}`);console.log(`Finished at: ${result.finishedAt}`);} catch (error) {console.error('Migration failed:', error);process.exit(1);}}
Run the script:
export HYGRAPH_AUTH_TOKEN="your-token"export HYGRAPH_ENDPOINT="https://your-region.hygraph.com/v2/..."node create-blog-schema.ts
#Integration test
Test the complete workflow:
- Create Author with social info and nested contact details
- Create Categories
- Create Post with all features:
- Set status to DRAFT (excerpt optional)
- Fill in title, slug, content, featured flag
- Select author and categories (relations)
- Select taxonomy categories (hierarchical)
- Add SEO metadata (metaTitle, metaDescription, keywords)
- Add content blocks (CallToAction and ImageBlock components)
- Test conditional visibility: Change status to PUBLISHED, verify excerpt becomes required
- Test workflow: Move through stages if configured
- Test webhook: Publish post and verify notification sent
- Verify final result: Post displays with author (nested contact), categories, taxonomy, SEO, content blocks, and all features working together
#Troubleshooting
#Wrong operation order
Operations must be executed in the correct order. For example, creating a relation before the target model exists will fail.
#Reused migration names
Use unique names for each migration, such as create-blog-schema-v1, create-blog-schema-v2, etc. Reusing names will cause the migration to fail.
const client = new Client({name: 'create-blog-schema', // First run: OK// Second run with same name: FAILS});
#apiId and apiIdPlural are the same
The values of apiId and apiIdPlural of a model must be different.
#Multiple title fields in a model
You can only have one isTitle: true field per model. This field is used as the entry identifier in the UI.
#Next steps
Review the Methods Reference for all available Management SDK operations.