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 likec4pnpm add likec4yarn add likec4bun add likec4You can initiate LikeC4 API from a directory with source files or from a string with DSL source.
From workspace
Section titled “From workspace”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 |
From source
Section titled “From source”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 * } }`)Dispose
Section titled “Dispose”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 resourcesawait 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.
Example
Section titled “Example”import { LikeC4 } from "likec4"
const likec4 = await LikeC4.fromSource(`....`)
// Validation errorsconsole.log(likec4.getErrors())
// Traverse the modelconst model = likec4.computedModel()
// Get elements of some kindconst 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 elementmodel .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 elementsTo 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 metadataconst 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 valuesconst elementsWithBackendTag = model.elements().filter(el => { const tags = el.getMetadata('tags') return Array.isArray(tags) ? tags.includes('backend') : tags === 'backend'})LikeC4Model
Section titled “LikeC4Model”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
Working with Element Metadata
Section titled “Working with Element Metadata”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 metadataconst api = model.element('api')
// Check if element has any metadataif (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 metadataconst 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 valuesconst 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 elementsconst 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())LikeC4DeploymentModel
Section titled “LikeC4DeploymentModel”API provides methods to query and traverse deployment model.
import { LikeC4 } from "likec4"
const likec4 = await LikeC4.fromSource(`....`)const model = likec4.computedModel()
// Get deployment modelconst deployment = model.deployment
// Get elements of some kindfor (const instance of deployment.instancesOf('cloud.backend.api')) { // ...}LikeC4ViewModel
Section titled “LikeC4ViewModel”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()) { // ... }}Model Builder
Section titled “Model Builder”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()import { Builder } from "@likec4/core/builder"
// Get composition functions for given specificationconst { model: { model, actor, system, component, rel, relTo, }, views: { view, viewOf, views, $include, $style, }, builder,} = Builder.forSpecification({ elements: { actor: { style: { shape: 'person', }, }, system: {}, component: {}, }, relationships: { likes: {}, }, tags: ['tag1', 'tag2', 'tag1'],})
const b1 = builder.with( model( actor('alice'), actor('bob'), rel('alice', 'bob', { tags: ['tag1'], kind: 'likes', }), system('cloud', { tags: ['tag1', 'tag2'] }).with( component('backend').with( component('api'), component('db'), rel('cloud.backend.api', 'cloud.backend.db') ), component('frontend').with( relTo('cloud.backend.api') ), ), ))
const b2 = b1.with( views( view('index', 'Index').with( $include('cloud.*'), ), viewOf('ui', 'cloud.ui').with( $include('* -> cloud.**'), $style('cloud.ui', { color: 'red' }), ), )).toLikeC4Model()You can mix both styles, depending on your preference and use cases.
Enriching a loaded workspace
Section titled “Enriching a loaded workspace”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
.c4source files.
Reading the parsed model
Section titled “Reading the parsed model”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 modelsconsole.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') // strictimport { 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')).
Relating new elements to existing ones
Section titled “Relating new elements to existing ones”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.
Finding and patching existing elements
Section titled “Finding and patching existing elements”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 stringconst 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 filenameawait 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/likec4await writeFile('./generated/model.c4', dsl) // or use writeDSL on the LikeC4 instanceLikeC4.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.