Skip to content

LikeC4 API

You can access and traverse your architecture model programmatically using the LikeC4 Model API.

Ensure you have likec4 in your dependencies:

npm i likec4

You can initiate LikeC4 API from a directory with source files or from a string with DSL source.

Recursively search and parse source files:

import { LikeC4 } from 'likec4'
const likec4 = await LikeC4.fromWorkspace('/path/to/workspace')

Method also accepts options:

Property Description
printErrors if model is invalid, errors are reported to the logger (default true)
throwIfInvalid return rejected promise if model is invalid (default false)
logger Whenever to use default (console), vite logger or your custom implementation
Disable with false
graphviz wasm (default) or binary - use local binaries of Graphviz (“dot”) or bundled WASM
watch Whether to watch for changes in the workspace. (default false)
mcp Whether to start MCP server.
- false - do not start MCP server (default)
- "stdio" - use stdio transport,
- {"port": number} - use http transport on specified port

Parse from the string:

import { LikeC4 } from "likec4"
const likec4 = await LikeC4.fromSource(`
specification {
element system
element user
}
model {
customer = user 'Customer'
cloud = system 'System'
}
views {
view index {
include *
}
}
`)

If you initialized LikeC4 with watch mode or enabled MCP server, you should dispose it:

import { LikeC4 } from 'likec4'
const likec4 = await LikeC4.fromWorkspace('/path/to/workspace', {
watch: true,
mcp: { port: 33335 },
})
// Cleanup resources
await likec4.dispose()

LikeC4 is automatically disposed with await using declaration:

import { LikeC4 } from 'likec4'
async function() {
await using likec4 = await LikeC4.fromWorkspace('/path/to/workspace', {
watch: true,
mcp: { port: 33335 },
})
// ...
// likec4 is disposed automatically
}

When the model is initialized, you can use the following methods to query and traverse it.

Two types of model (with similar API):

  • LikeC4Model.Computed - includes computed views (from predicates), fast, synchronous, enough to traverse but not ready for rendering.
  • LikeC4Model.Layouted - extends computed model with layout data (dimensions, positions), that is needed for rendering.
import { LikeC4 } from "likec4"
const likec4 = await LikeC4.fromSource(`....`)
// Validation errors
console.log(likec4.getErrors())
// Traverse the model
const model = likec4.computedModel()
// Get elements of some kind
const elements = model.elementsOfKind('kind1')
// Use where operator to filter elements:
// kind is 'kind1' and (tag is 'tag2' or tag is not 'tag3')
const elements = model.elementsWhere({
and: [
{ kind: 'kind1' },
{
or: [
{ tag: 'tag2' },
{
tag: {
neq: 'tag3',
},
},
],
},
],
})
// Get views that include the element
model
.element('cloud.backend.api')
.views()
// Get source elements of incoming relationships (filter by tags)
model
.element('cloud.backend.api')
.incoming() // relationships incoming to the element
.filter(r => r.isTagged('http')) // filter by tags
.map(r => r.source) // get source elements

To get layouted model:

import { LikeC4 } from "likec4"
const likec4 = await LikeC4.fromSource(`....`)
const model = await likec4.layoutedModel()
const diagram = model.view('index')
// Working with metadata (including array values)
const element = model.element('cloud.backend.api')
// Get all metadata
const allMetadata = element.getMetadata()
console.log(allMetadata)
// Output: { version: '3.2.1', tags: ['backend', 'gateway'], regions: ['us-east-1', 'eu-west-1'] }
// Get specific metadata field (could be string or string[])
const tags = element.getMetadata('tags')
if (Array.isArray(tags)) {
console.log(`Element has ${tags.length} tags: ${tags.join(', ')}`)
} else if (tags) {
console.log(`Element has single tag: ${tags}`)
}
// Filter elements by array metadata values
const elementsWithBackendTag = model.elements().filter(el => {
const tags = el.getMetadata('tags')
return Array.isArray(tags)
? tags.includes('backend')
: tags === 'backend'
})

Model API provides methods to query and traverse the whole model.

interface LikeC4Model {
/**
* Returns the root elements of the model.
*/
roots(): Element[];
/**
* Returns all elements in the model.
*/
elements(): Element[];
/**
* Returns a specific element by its FQN.
*/
element(id: Fqn): Element;
/**
* Returns all relationships in the model.
*/
relationships(): Relationship[];
/**
* Returns a specific relationship by its ID.
*/
relationship(id: RelationID): Relationship;
/**
* Returns all views in the model.
*/
views(): ReadonlyArray<LikeC4ViewModel>;
/**
* Returns a specific view by its ID.
*/
view(viewId: ViewID): LikeC4ViewModel;
/**
* Returns the parent element of given element.
* @see ancestors
*/
parent(element: ElementOrFqn): Element | null;
/**
* Get all children of the element (only direct children),
* @see descendants
*/
children(element: ElementOrFqn): Element[];
/**
* Get all sibling (i.e. same parent)
*/
siblings(element: ElementOrFqn): Element[];
/**
* Get all ancestor elements (i.e. parent, parent’s parent, etc.)
* (from closest to root)
*/
ancestors(element: ElementOrFqn): Element[];
/**
* Get all descendant elements (i.e. children, children’s children, etc.)
*/
descendants(element: ElementOrFqn): Element[];
/**
* Incoming relationships to the element and its descendants
* @see incomers
*/
incoming(element: ElementOrFqn, filter?: 'all' | 'direct' | 'to-descendants'): Relationship[];
/**
* Source elements of incoming relationships
*/
incomers(element: ElementOrFqn, filter?: 'all' | 'direct' | 'to-descendants'): Element[];
/**
* Outgoing relationships from the element and its descendants
* @see outgoers
*/
outgoing(element: ElementOrFqn, filter?: 'all' | 'direct' | 'from-descendants'): Relationship[];
/**
* Target elements of outgoing relationships
*/
outgoers(element: ElementOrFqn, filter?: 'all' | 'direct' | 'from-descendants'): Element[];
}

Check sources for methods - LikeC4Model

Elements can have metadata with both single string values and string arrays. The API provides convenient methods to access and work with this metadata:

import { LikeC4 } from "likec4"
const likec4 = await LikeC4.fromSource(`
specification {
element service
element application
}
model {
api = service 'API Gateway' {
metadata {
version '3.2.1'
maintainer 'Platform Team'
tags ['backend', 'gateway', 'microservice']
regions ['us-east-1', 'eu-west-1']
critical true
}
}
frontend = application 'Frontend' {
metadata {
framework 'React'
features ['auth', 'dashboard', 'reports']
team_members ['alice', 'bob', 'carol']
release_branch 'main'
}
}
}
`)
const model = likec4.computedModel()
// Get element and access metadata
const api = model.element('api')
// Check if element has any metadata
if (api.hasMetadata()) {
console.log('API has metadata')
// Get all metadata as an object
const metadata = api.getMetadata()
console.log('All metadata:', metadata)
// Get specific metadata fields
const version = api.getMetadata('version') // string: '3.2.1'
const tags = api.getMetadata('tags') // string[]: ['backend', 'gateway', 'microservice']
const regions = api.getMetadata('regions') // string[]: ['us-east-1', 'eu-west-1']
// Handle array metadata values
if (Array.isArray(tags)) {
console.log(`API has ${tags.length} tags:`)
tags.forEach(tag => console.log(` - ${tag}`))
// Check if specific value exists in array
if (tags.includes('backend')) {
console.log('API is tagged as backend service')
}
}
// Handle mixed metadata types
const handleMetadataValue = (key: string, value: string | string[] | undefined) => {
if (Array.isArray(value)) {
return `${key}: [${value.join(', ')}]`
} else if (value) {
return `${key}: ${value}`
}
return `${key}: undefined`
}
console.log(handleMetadataValue('version', version))
console.log(handleMetadataValue('tags', tags))
console.log(handleMetadataValue('regions', regions))
}
// Advanced filtering using metadata
const backendServices = model.elements()
.filter(element => {
const tags = element.getMetadata('tags')
return Array.isArray(tags) ? tags.includes('backend') : tags === 'backend'
})
const multiRegionServices = model.elements()
.filter(element => {
const regions = element.getMetadata('regions')
return Array.isArray(regions) && regions.length > 1
})
// Group elements by metadata values
const elementsByFramework = new Map<string, typeof model.elements>()
for (const element of model.elements()) {
const framework = element.getMetadata('framework')
if (typeof framework === 'string') {
if (!elementsByFramework.has(framework)) {
elementsByFramework.set(framework, [])
}
elementsByFramework.get(framework)!.push(element)
}
}
// Collect all unique tags from all elements
const allTags = new Set<string>()
for (const element of model.elements()) {
const tags = element.getMetadata('tags')
if (Array.isArray(tags)) {
tags.forEach(tag => allTags.add(tag))
} else if (typeof tags === 'string') {
allTags.add(tags)
}
}
console.log('All unique tags:', Array.from(allTags).sort())

API provides methods to query and traverse deployment model.

import { LikeC4 } from "likec4"
const likec4 = await LikeC4.fromSource(`....`)
const model = likec4.computedModel()
// Get deployment model
const deployment = model.deployment
// Get elements of some kind
for (const instance of deployment.instancesOf('cloud.backend.api')) {
// ...
}

View model API provides methods to query and traverse elements and relationships that are included in the view.

import { LikeC4 } from "likec4"
const likec4 = await LikeC4.fromSource(`....`)
const model = likec4.computedModel()
for (const view of model.views()) {
if (view.isDynamicView()) {
// ...
}
}

Type-safe builder available from @likec4/core/builder (and likec4/model/builder).
Builder can be used to create model programmatically and supports two styles:

import { Builder } from "@likec4/core/builder"
const m = Builder
.specification({
elements: {
actor: {
style: {
shape: 'person',
},
},
system: {},
component: {},
},
relationships: {
likes: {},
},
tags: ['tag1', 'tag2', 'tag1'],
})
.model(({ actor, system, component, relTo, rel }, _) =>
_(
actor('alice'),
actor('bob'),
rel('alice', 'bob', {
tags: ['tag1'], // you get code completion for tags
kind: 'likes', // code completion for kind
}),
system('cloud', { tags: ['tag1', 'tag2'] }).with(
component('backend').with(
component('api'),
component('db'),
// code completion for relationships
rel('cloud.backend.api', 'cloud.backend.db')
),
component('frontend').with(
relTo('cloud.backend.api')
),
),
)
)
.views(({ view, viewOf, $include, $style }, _) =>
_(
view('index', 'Index').with(
$include('cloud.*'),
),
viewOf('ui', 'cloud.ui').with(
// code completion for predicates
$include('* -> cloud.**'),
$style('cloud.ui', { color: 'red' }),
),
)
)
.toLikeC4Model()

You can mix both styles, depending on your preference and use cases.

You can load an existing LikeC4 workspace, hand the parsed model back to the Model Builder, add elements/relations/views programmatically, and either keep working with the resulting model in memory or emit DSL source back to disk.

Typical use cases:

  • Overlay infrastructure pulled from Terraform state, AWS or Kubernetes onto a hand-written model.
  • Merge multiple teams’ models and add cross-team relationships.
  • Generate views by convention (one view per team, per environment, etc.).
  • Patch a model from an external service catalogue and re-emit .c4 source files.

likec4.parsedModel(project?) returns the parsed-stage model (elements, relations, views, deployments, globals and imports) before view computation:

import { LikeC4 } from "likec4"
const likec4 = await LikeC4.fromWorkspace('./')
const parsed = await likec4.parsedModel()
// element/relation lookups work the same as on computed models
console.log(parsed.element('cloud.frontend').kind)
console.log(Object.keys(parsed.$data.views))

LikeC4.toBuilder() — get a Builder seeded from disk

Section titled “LikeC4.toBuilder() — get a Builder seeded from disk”

likec4.toBuilder(mode?, project?) returns a Model Builder pre-populated with the loaded workspace. You can then chain .model(...), .deployment(...) and .views(...) to add new content, and call .toLikeC4Model() (or .build()) to get the enriched model.

The builder is editable by default — re-declaring an element that already exists (same FQN, same kind) edits it in place rather than throwing, which is what you usually want when patching a loaded model. Pass 'strict' to get the strict builder where duplicate FQNs always throw:

const editable = await likec4.toBuilder() // editable (default)
const strict = await likec4.toBuilder('strict') // strict
import { LikeC4 } from "likec4"
const likec4 = await LikeC4.fromWorkspace('./')
const enriched = (await likec4.toBuilder())
.model(({ system, component }, _) =>
_(
system('monitoring').with(
component('grafana'),
component('prometheus'),
),
)
)
.views(({ view, $include }, _) =>
_(
view('monitoring', 'Monitoring').with(
$include('monitoring.*'),
),
)
)
.toLikeC4Model()

The same seeding entry is also available directly on the Builder for when you already have the parsed data in hand:

import { Builder } from "@likec4/core/builder"
const builder = Builder.fromParsed(parsedData)

Editable vs strict — editing seeded elements

Section titled “Editable vs strict — editing seeded elements”

toBuilder() is editable by default, so you can edit seeded entries (for example, to update a title or merge in new children) just by re-declaring them:

import { LikeC4 } from "likec4"
const likec4 = await LikeC4.fromWorkspace('./')
const updated = (await likec4.toBuilder())
.model(({ component }, _) =>
_(
component('cloud.api', { title: 'Updated' }),
component('cloud.ui').with(
component('react'),
),
)
)
.toLikeC4Model()

In editable mode, same-FQN + same-kind redeclaration replaces the element’s properties. Redeclaring with a different kind still throws (it’s almost certainly a programmer error). If you want re-declaring any existing FQN to throw, use the strict builder via await likec4.toBuilder('strict') (or Builder.fromParsed(data, 'strict')).

A common task is adding a new element and connecting it to something already in the DSL. Existing FQNs aren’t in the static type set, but you can reference them as plain strings — the relationship is validated against the seeded model when you build:

import { LikeC4 } from "likec4"
const likec4 = await LikeC4.fromWorkspace('./')
const enriched = (await likec4.toBuilder())
.model(({ system, relTo }, _) =>
_(
system('monitoring').with(
// `cloud.backend` is an existing element loaded from the DSL
relTo('cloud.backend', 'observes'),
),
)
)
.toLikeC4Model()
// or, equivalently, from the top level via `rel`:
// .model(({ system, rel }, _) =>
// _(
// system('monitoring'),
// rel('monitoring', 'cloud.backend', 'observes'),
// )
// )

Referencing an FQN that doesn’t exist throws at build time, so typos surface immediately even though the compiler can’t catch them.

To discover what’s already in the model, walk it through the LikeC4Model returned by computedModel() (or inspect the parsed shape via parsedModel()):

const model = await likec4.computedModel()
for (const element of model.elements()) {
console.log(element.id, element.kind, element.title)
}

Because editing replaces rather than merges (see the note above), patching a single property means reading the existing element and passing back the values you want to keep alongside your change:

const existing = (await likec4.parsedModel()).element('customer')
const updated = (await likec4.toBuilder())
.model(({ actor }, _) =>
_(
actor('customer', {
title: existing.title, // keep what you are not changing
metadata: { team: 'platform' }, // add / patch
}),
)
)
.toLikeC4Model()

Builder.specification() — add new kinds and tags

Section titled “Builder.specification() — add new kinds and tags”

The instance method .specification(spec) extends an existing builder with extra element / deployment / relationship kinds, tags and metadata keys. Existing keys with the same name are overridden; new keys are added. Useful when the workspace’s specification is a starting point and you want to introduce overlay-specific kinds:

import { LikeC4 } from "likec4"
const likec4 = await LikeC4.fromWorkspace('./')
const model = (await likec4.toBuilder())
.specification({
elements: {
database: { style: { shape: 'storage' } },
},
tags: {
experimental: { color: '#f00' },
},
})
.model(({ database }, _) =>
_(database('warehouse', { tags: ['experimental'] })),
)
.toLikeC4Model()

The returned builder’s types are the union of the prior types and the newly-declared spec, so element-kind helpers, tags and metadata keys accumulate across chained .specification(...) calls instead of being narrowed.

LikeC4.toDSL() and writeDSL() — render DSL back

Section titled “LikeC4.toDSL() and writeDSL() — render DSL back”

likec4.toDSL(project?) renders the parsed model back to LikeC4 DSL source. To write that source to disk, use the Node-only writeDSL helper exported from likec4:

import { LikeC4, writeDSL } from "likec4"
const likec4 = await LikeC4.fromWorkspace('./')
// Render to string
const dsl = await likec4.toDSL()
// Or write to a file (default: ${targetDir}/model.c4)
const outPath = await writeDSL(likec4, './generated')
console.log(`Wrote DSL to ${outPath}`)
// Custom filename
await writeDSL(likec4, './generated', { fileName: 'snapshot.c4' })

You can combine the pieces above to load, enrich, and re-emit in a single pipeline:

const enriched = (await likec4.toBuilder())
.specification({ tags: { autogenerated: { color: '#999' } } })
.model(({ component }, _) =>
_(component('cloud.new-service', { tags: ['autogenerated'] })),
)
const data = enriched.build()
const dsl = generateLikeC4(data) // from @likec4/generators/likec4
await writeFile('./generated/model.c4', dsl) // or use writeDSL on the LikeC4 instance

LikeC4.toTypedBuilder() — get a typed Builder

Section titled “LikeC4.toTypedBuilder() — get a typed Builder”

When the parsed model comes from a workspace, element kinds and FQNs are only known at runtime — so the builder returned by LikeC4.toBuilder() has loose types (Builder<AnyTypes>). If your code authors the model with the same specification as the DSL, pass that spec to toTypedBuilder and get a builder typed by it — with autocomplete on element/deployment/relationship kinds, tags and metadata keys:

const builder = await likec4.toTypedBuilder({
specification: {
elements: ['actor', 'system', 'component'],
tags: ['external'],
},
})
// `system` / `component` are statically known — no cast needed.
builder.model(({ system, component }, _) =>
_(system('monitoring').with(component('grafana'))),
)

The given specification is validated against the loaded model (subset semantics): every kind / tag / metadata key you declare must exist in the loaded model, otherwise the call throws — so the types can’t silently drift away from the DSL. The loaded model may contain extra kinds you didn’t declare; you just won’t get typed helpers for them. mode defaults to 'editable', same as toBuilder.