Structurizr DSL

Structurizr DSL is a plain-text way to describe a C4 model — used by Structurizr Lite, Structurizr Cloud, and a wide ecosystem of docs-as-code tooling. CRAYON-C4 can import a .dsl workspace as a new project (or paste it into an existing one), and export any project back to one — including its F4 behavioural flows and decisions — so your model can move between CRAYON and the Structurizr ecosystem.

Both directions are free on every tier — a .dsl workspace is a portable text file, the same kind of thing as the JSON model file.

Importing a workspace

Open Projects → New project → From file and pick a .dsl or .structurizr file (a file with no extension is fine too — the app reads the content and recognizes a workspace block on its own). The file is parsed, converted, and run through the same validation every other import goes through, so you always land on a valid, canonical project.

Keywords are read case-insensitively — softwaresystem, SoftwareSystem and softwareSystem all mean the same thing, matching real Structurizr DSL and the spelling many real-world workspaces actually use (including Structurizr's own "Big Bank plc" example).

A model that declares elements but no views { ... } block at all still lands somewhere you can see it: CRAYON runs the same "wand" a diagram-less project's own right-click menu offers, so the imported elements get at least one diagram rather than opening to an empty canvas.

Next to the file picker is an "Import the workspace's colours" checkbox (checked by default). Uncheck it to bring in every element, tag and relationship without applying the colours from the workspace's styles { ... } block — useful when you'd rather draw the imported model in CRAYON's own default palette than the author's. A displayType a tag names is still recorded on the element either way (so you can style it yourself in Settings later); only the colour itself is skipped.

One line that can't be resolved doesn't cost you the rest of the file. A relationship naming an element that turns out not to exist, a view referencing something that never resolved — those are reported, skipped, and everything else in the file still imports normally; you'll see a note naming how many lines were skipped. The import is only ever REFUSED outright when there is genuinely nothing to bring in: no model block at all, the whole thing isn't DSL, or every single element the file describes turns out to be unreachable (the workspace extends case below is the common way that happens) — and when that's why, the message says so specifically rather than pointing at one arbitrary unresolved line.

A .zip is also accepted there — the bundle shape described in Decisions, carrying workspace.dsl plus the folder of decision files its !decisions line points at.

Adding elements to an existing project

You don't have to create a whole new project to bring in DSL — on a project's own page, Paste Structurizr DSL parses pasted text the same way and adds what it describes to the project you already have: new elements, relationships, diagrams and dynamic views. Nothing already in the project is ever removed, and if something in the pasted text would change an element you've since edited here, you're shown a review before anything is written — the same review "Update from code" already uses. It carries the same "Import the workspace's colours" checkbox.

Exporting to a workspace

Open a project's detail page, click Export, and choose Export Structurizr DSL. You get a .dsl file naming every element, relationship, diagram, deployment environment and behavioural flow in the project, ready to open in Structurizr Lite or check into a repository. If the project has any decisions, you get a .zip bundle instead — see below.

Try it — downloadable samples

Rather than typing these examples in by hand, download them and use New project → From file:

What's supported

The model

DSL constructWhat it becomes in CRAYON
person "Name" "description" "tags"A Person element
softwareSystem "Name" "description" "tags"A Software System (or External System — see below)
container "Name" "description" "technology" "tags" (nested in a softwareSystem)A Container, parented to that system
component "Name" "description" "technology" "tags" (nested in a container)A Component, parented to that container
a -> b "description" "technology" "tags"A relationship between the two elements
this -> b / a -> this (inside an element's own block)A relationship using "the element whose block this is" as one end
group "Label" { ... }A boundary group wrapping its members, on whichever diagram draws them as peers
tags "…" / a trailing tags argumentThe element's display type (when a tag names one you styled) plus any leftover tags become CRAYON tags (#slug)
description "…" / technology "…" (as child statements)Set the element's description / technology when not given inline
url "…"Appended to the element's description as a link
enterprise "Name" { ... }Its contents are imported flat — CRAYON has no enterprise-boundary concept (see Restrictions)
element "Name" { ... } (a top-level amendment)Applied to the already-declared element of that name

Deployment

DSL constructWhat it becomes in CRAYON
deploymentEnvironment "Name" { ... }A deployment environment (its own canvas)
deploymentNode "Name" "description" "technology" "tags" instances { ... }A deployment node, nestable to any depth; the tags become CRAYON tags and instances becomes its replica-count badge
infrastructureNode "Name" ...An infrastructure node
containerInstance <ref> / softwareSystemInstance <ref>An instance of the referenced C4 container/system, dropped onto the deployment canvas
a -> b between deployment elementsA connection on that environment's canvas

Views and styles

DSL constructWhat it becomes in CRAYON
systemLandscapeA diagram with no subject, showing every top-level element
systemContext <system>A diagram whose subject is that system
container <system>A diagram whose subject is that system, at container level
component <container>A diagram whose subject is that container
deployment <system|*> "<environment>"Confirms the named deployment environment exists
include *Populates the diagram the way CRAYON's own "populate from subject" wand does — the subject's direct children, plus anything with a relationship to the subject, one of its ancestors, or one of its descendants (this matches Structurizr's own include * behavior for scoped views)
include <id> <id> … / exclude <id> …An explicit node list, added or removed
dynamic <scope> "<key>" { ... }An F4 behavioural flow — see Behavioural flows (F4) below
styles { element "Tag" { background … color … stroke … border … } }A tag matching one of the six fixed element types styles that type directly; any other tag becomes a CRAYON display type
styles { relationship "Tag" { color … thickness … style … } }A tag named "Relationship" sets the project's default line style; any other name becomes a CRAYON line type

A worked example

workspace "Getting Started" "This is a model of my software system." {

    model {
        user = person "User" "A user of my software system."
        mySystem = softwareSystem "Software System" "My software system." {
            webapp = container "Web Application" "Delivers content to users." "Java and Spring MVC"
            database = container "Database" "Stores data." "PostgreSQL" {
                tags "Database"
            }
            webapp -> database "Reads from and writes to" "JDBC"
        }
        user -> mySystem "Uses"
    }

    views {
        systemContext mySystem "SystemContext" {
            include *
            autoLayout
        }
        container mySystem "Containers" {
            include *
            autoLayout
        }

        styles {
            element "Person" {
                background #08427b
                color #ffffff
            }
            element "Database" {
                background #4d940c
                color #ffffff
            }
        }
    }
}

Importing this creates:

  • Four elements: User (Person), Software System (Software System), Web Application and Database (Containers, with their technologies), and one relationship Web Application -> Database plus User -> Software System
  • Two diagrams: SystemContext (subject = Software System, showing User as its one neighbour) and Containers (subject = Software System, showing both containers and User, since User has a relationship with the system itself — that's real Structurizr behavior for include *, not a CRAYON quirk)
  • Person elements styled navy-on-white; the Database-tagged container gets its own green display type

Exporting that same project back to DSL reproduces it almost verbatim — see Round-trip fidelity for exactly what changes and why.

A deployment example

workspace "Getting Started" {
    model {
        user = person "User"
        mySystem = softwareSystem "Software System" {
            webapp = container "Web Application" "" "Java and Spring MVC"
            database = container "Database" "" "PostgreSQL"
        }
        user -> mySystem "Uses"

        deploymentEnvironment "Live" {
            deploymentNode "Web Server" "" "Ubuntu 22.04" {
                containerInstance webapp
            }
            deploymentNode "Database Server" "" "Ubuntu 22.04" {
                containerInstance database
            }
        }
    }
    views {
        deployment mySystem "Live" {
            include *
            autoLayout
        }
    }
}

This creates one deployment environment, Live, with two root nodes ("Web Server" and "Database Server"), each holding a dropped instance of the matching C4 container — visible immediately as a deployment diagram, no extra steps.

Behavioural flows (F4)

Structurizr's dynamic view — its answer to a sequence diagram — is CRAYON's F4 behavioural model in DSL form. A step is a numbered handoff between two elements; F4 is one lane per element with an activity in it for each step:

dynamic shop "PlaceOrder" "Placing an order" {
    customer -> webapp "Submits an order"
    webapp -> api "Places the order"
    api -> database "Reads from and writes to"
    autoLayout
}

Importing this builds an F4 process with one lane per element (Customer, Web Application, API, Database) and one activity per step, connected in order. Exporting an F4 diagram back to DSL is the same translation in reverse — one dynamic view per F4 diagram, its steps numbered by the order things happen in your flow.

This is a deliberately narrow translation, on purpose: a dynamic view only knows about C4 elements, not F4's own activities, lanes, boundary events, groups, tags or exact positions, so:

  • Individual F4 activities collapse onto their element. Two activities on the same lane produce no step at all — a dynamic view only shows what crosses from one element to another.
  • A step only exports when the C4 model already has a matching relationship between the two elements. An F4 hop the model doesn't cover is reported and left out — add the relationship (or accept CRAYON's "add the missing steps" suggestion when you draw the connection) to include it.
  • A call step's dashed return is written as its own reversed line. F4 shows a call's answer without a separate connection; DSL has no such shorthand, so export adds one, and import reads a step immediately reversing the one before it back into a single call with its return.
  • Boundary events, groups, tags, notes and exact layout have no DSL equivalent and are never carried either way.

Decisions

CRAYON's own Decisions (ADR) feature maps onto Structurizr's !decisions directive — but only ever through the bundle shape, because Structurizr decisions are never inline text in the .dsl file itself; the directive always points at an external folder of markdown files.

Export: when a project has any decisions, Export Structurizr DSL downloads a .zip instead of a plain .dsl — workspace.dsl (carrying !decisions adr madr) plus an adr/ folder with one markdown file per decision, in the same format CRAYON's own Markdown export already writes them in. That format happens to be real YAML front matter carrying status/date, which is exactly what Structurizr's own MADR importer reads — so the bundle opens in a real Structurizr installation too, which simply ignores CRAYON's own extra fields (id, categories, links).

Import: only from a .zip bundle — a plain .dsl text file has no folder to resolve !decisions against, so its directive is reported and skipped, same as always. Only CRAYON's own decision-file format is understood; a decisions folder from a real-world adr-tools or plain-MADR project (not produced by this app) is reported and skipped file-by-file rather than guessed at.

Restrictions

Structurizr DSL is a large language; CRAYON covers the parts that map onto its own model. Anything else is skipped and reported — never silently dropped and never a failed import — so you always see exactly what didn't make the trip:

  • Filtered, custom and image views — CRAYON's diagrams are always scoped-by-subject; there's no equivalent to a filtered variant of another view, a freeform custom view, or an embedded image view. (dynamic views ARE supported — see Behavioural flows (F4).)
  • Themes, branding and terminology — CRAYON has its own design system and doesn't apply an external Structurizr theme URL or replace its own vocabulary ("Container", "Component", …) with custom terms.
  • !include of external files, and workspace extends <file> / !ref <id> — Structurizr's multi-file decomposition is not fetched or inlined; a workspace that extends another file imports only what it declares directly, with a clear note that its !ref'd content is missing. If every element the file describes turns out to live only in the extended file, there is nothing left to import at all — that's refused outright, naming the extends line as the reason, rather than reported as if it had worked. Structurizr's own examples repo happens to ship the "Big Bank plc" workspace as BOTH shapes: model.dsl and internet-banking-system/workspace.dsl are extends-based fragments of each other (neither stands alone), while workspace.dsl at the top level is a combined, self-contained file already carrying everything both fragments describe — that's the one to import; there's no fragment left to combine it with.
  • !script — no scripting is executed.
  • !docs — CRAYON has its own descriptions on every element rather than a separate documentation tree; not converted.
  • !decisions / !adrs — supported only through the .zip bundle shape (see Decisions); referenced in a plain .dsl text file, the directive is reported and skipped, since there's no folder alongside a single text file to resolve it against.
  • Properties and perspectives — not represented; noted in the import report.
  • Hierarchical identifiers (!identifiers hierarchical) — CRAYON reads every identifier as an opaque flat key. A dotted or fully-qualified identifier still works; a relative reference that only resolves under hierarchical scoping will not.
  • Enterprise boundaries — enterprise { ... } contents import flat, with no internal/external boundary drawn. Use the "External" tag convention below instead.
  • Code-level elements — Structurizr's own DSL model stops at Component (there is no code keyword), so CRAYON's Code-level elements have nothing to export to and are reported as omitted. This mirrors Structurizr itself, not a CRAYON limitation.
  • F4 activities, lanes, boundary events, groups, tags, notes and exact layout — a dynamic view only carries the ELEMENT-to-element story (see Behavioural flows (F4)); everything else about how an F4 board is drawn stays CRAYON-only.

The "External System" convention

Structurizr DSL has no dedicated keyword for an external system — location is usually expressed by a convention. CRAYON's import/export uses one consistently: a softwareSystem tagged "External" becomes a CRAYON External System, and exporting one always adds that tag back:

email = softwareSystem "E-mail System" "The internal e-mail system." "External"

Round-trip fidelity

Import → export → import is designed to preserve everything DSL can express: element names, hierarchy, relationships (with technology and description), groups, deployment trees, and element/relationship styles all survive intact — verified by CRAYON's own test suite against a multi-system example with groups, an enterprise boundary, nested deployment nodes, and custom styles.

What changes, by design:

  • Identifiers are regenerated. Export mints fresh, readable lowerCamelCase identifiers from each element's name rather than preserving the ones your original file used.
  • include becomes explicit. CRAYON always exports the exact node list a diagram draws (include database user webApplication) rather than a bare include * — so what you see on export is guaranteed to be what re-imports, even if a future CRAYON change altered the "populate from subject" heuristic.
  • Only customized styles are written. A style block appears only for a fixed element type you actually changed from CRAYON's own default palette — an untouched project exports no styles block at all.
  • F4 flows round-trip at the ELEMENT level, not the activity level. A dynamic view carries which elements hand off to each other and in what order, so re-importing an exported flow rebuilds one lane per element and one activity per step — not the original board's own lanes, activity names or positions.
  • Layout, notes, tags, and F4 boundary events/groups don't round-trip through DSL. These have no Structurizr equivalent; use JSON export when you need a fully lossless copy of a CRAYON project.

Where next