Help teams manage content creation and approval in a clear and structured way
Hygraph
Docs

#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:

  • Post model with title, content, status, and a featured flag
  • Author model with name, bio, and email
  • Category model with name and description
  • SeoMetadata component (reusable SEO fields)
  • AuthorInfo component (social media info)
  • CallToAction and ImageBlock components (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.
  • initialValue sets the default for new entries.
  • For boolean fields, use true or false.

#Create an enumeration

Create an enumeration before configuring fields that reference them.

// Create PostStatus enumeration
client.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 component
client.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 infographic
  • CallToAction: 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 component
client.createComponent({
apiId: 'ContactDetails',
apiIdPlural: 'ContactDetailsCollection',
displayName: 'Contact Details',
description: 'Phone and email contact information',
});
// Add fields to the component
client.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 required
description: '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 component
client.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 string
reverseField: {
apiId: 'posts',
modelApiId: 'Author', // Required! Specify the related model
displayName: 'Posts',
isList: true, // Required! Author has many posts
visibility: VisibilityTypes.READ_WRITE // Use visibility instead of isHidden
},
isList: false, // Post has one author
isRequired: 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 string
reverseField: {
apiId: 'posts',
modelApiId: 'Category', // Required! Specify the related model here
displayName: 'Posts',
isList: true, // Required! Category has many posts
visibility: 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 once
client.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 model
client.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 string
allowEdit: true, // Required!
allowedRoles: ['role-id-1'], // Required! Array of role IDs
position: 0
},
{
apiId: 'review',
displayName: 'In Review',
description: 'Awaiting editorial review',
color: ColorPalette.YELLOW, // Use enum
allowEdit: false, // Required!
returnToStep: 'draft', // Can return to draft if rejected
allowedRoles: ['role-id-2'], // Required!
position: 1
},
{
apiId: 'approved',
displayName: 'Approved',
description: 'Ready for publication',
color: ColorPalette.GREEN, // Use enum
allowEdit: false, // Required!
allowedRoles: ['role-id-3'], // Required!
publishStages: ['published'], // Optional: stages that can be published from this step
position: 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 running
process.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 migration
const 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:

  1. Create Author with social info and nested contact details
  2. Create Categories
  3. 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)
  4. Test conditional visibility: Change status to PUBLISHED, verify excerpt becomes required
  5. Test workflow: Move through stages if configured
  6. Test webhook: Publish post and verify notification sent
  7. 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.