diff --git a/libs/@hashintel/petrinaut-core/README.md b/libs/@hashintel/petrinaut-core/README.md
index 4b95ac21d45..7c5d1d57ff9 100644
--- a/libs/@hashintel/petrinaut-core/README.md
+++ b/libs/@hashintel/petrinaut-core/README.md
@@ -5,3 +5,31 @@ language services, and supporting domain utilities.
This package intentionally has no React or UI dependencies. The visual editor
package, `@hashintel/petrinaut`, builds on top of it.
+
+## Handle Creation
+
+Petrinaut reads and writes documents through `PetrinautDocHandle`. Use
+`createJsonDocHandle()` for an in-memory handle with patch events,
+extension sanitization, and optional undo/redo history:
+
+```ts
+import { createJsonDocHandle } from "@hashintel/petrinaut-core";
+
+const handle = createJsonDocHandle({
+ id: "my-net",
+ initial: {
+ places: [{ id: "p1", name: "P1", x: 0, y: 0 }],
+ transitions: [],
+ },
+});
+```
+
+`initial` accepts `SDCPNInput`, a loose document shape for host integrations.
+Plain-net defaults are filled in automatically: omitted arc weights become `1`,
+input arc types become `"standard"`, extension arrays default to `[]`, and
+disabled extension data is sanitized according to handle capabilities.
+
+When another application is the source of truth, implement `PetrinautDocHandle`
+directly so editor edits can emit `source: "local"` and host/store updates can
+emit `source: "remote"`. The visual editor package has a fuller guide in
+`@hashintel/petrinaut/INTEGRATION.md`.
diff --git a/libs/@hashintel/petrinaut-core/src/handle/json-doc-handle/create-json-doc-handle.ts b/libs/@hashintel/petrinaut-core/src/handle/json-doc-handle/create-json-doc-handle.ts
index b35bba7b7af..460192bd946 100644
--- a/libs/@hashintel/petrinaut-core/src/handle/json-doc-handle/create-json-doc-handle.ts
+++ b/libs/@hashintel/petrinaut-core/src/handle/json-doc-handle/create-json-doc-handle.ts
@@ -12,8 +12,10 @@ import {
type PetrinautHandleCapabilities,
} from "../../extensions";
import { createReadableStore } from "../../store";
+import { normalizeSDCPN } from "../../types/sdcpn-input";
import type { SDCPN } from "../../types/sdcpn";
+import type { SDCPNInput } from "../../types/sdcpn-input";
import type {
DocChangeEvent,
DocHandleState,
@@ -50,7 +52,14 @@ const DEFAULT_HISTORY_LIMIT = 50;
export type CreateJsonDocHandleOptions = {
id?: DocumentId;
- initial: SDCPN;
+ /**
+ * Initial document. Accepts a loose {@link SDCPNInput} — extension fields
+ * (`colorId`, `lambdaCode`, arc `type`/`weight`, the `types` /
+ * `parameters` / `differentialEquations` arrays, ...) may be omitted and are
+ * filled with plain-net defaults via {@link normalizeSDCPN}. A complete
+ * {@link SDCPN} is a valid input too.
+ */
+ initial: SDCPNInput;
capabilities?: PetrinautHandleCapabilities;
/**
* Maximum number of history checkpoints retained. Older entries are dropped
@@ -82,7 +91,7 @@ export function createJsonDocHandle(
const subscribers = new Set<(event: DocChangeEvent) => void>();
let current: SDCPN = sanitizeSDCPNForExtensions(
- opts.initial,
+ normalizeSDCPN(opts.initial),
resolvedCapabilities.extensions,
);
diff --git a/libs/@hashintel/petrinaut-core/src/index.ts b/libs/@hashintel/petrinaut-core/src/index.ts
index ff2de8ba260..3592006eb73 100644
--- a/libs/@hashintel/petrinaut-core/src/index.ts
+++ b/libs/@hashintel/petrinaut-core/src/index.ts
@@ -261,6 +261,14 @@ export type {
// --- Domain types ---
export type * from "./types/sdcpn";
+export { normalizeSDCPN } from "./types/sdcpn-input";
+export type {
+ SDCPNInput,
+ SDCPNInputArcInput,
+ SDCPNOutputArcInput,
+ SDCPNPlaceInput,
+ SDCPNTransitionInput,
+} from "./types/sdcpn-input";
export { parseArcId } from "./types/selection";
export type * from "./types/selection";
diff --git a/libs/@hashintel/petrinaut-core/src/types/sdcpn-input.test.ts b/libs/@hashintel/petrinaut-core/src/types/sdcpn-input.test.ts
new file mode 100644
index 00000000000..8f019d8f780
--- /dev/null
+++ b/libs/@hashintel/petrinaut-core/src/types/sdcpn-input.test.ts
@@ -0,0 +1,141 @@
+import { describe, expect, it } from "vitest";
+
+import { isSDCPNEqual } from "../lib/deep-equal";
+
+import { normalizeSDCPN } from "./sdcpn-input";
+
+import type { SDCPN } from "./sdcpn";
+
+describe("normalizeSDCPN", () => {
+ it("fills plain-net defaults for omitted place and transition fields", () => {
+ const result = normalizeSDCPN({
+ places: [{ id: "p1", name: "P1", x: 1, y: 2 }],
+ transitions: [
+ {
+ id: "t1",
+ name: "T1",
+ inputArcs: [{ placeId: "p1" }],
+ outputArcs: [{ placeId: "p1" }],
+ x: 3,
+ y: 4,
+ },
+ ],
+ });
+
+ expect(result.places[0]).toEqual({
+ id: "p1",
+ name: "P1",
+ colorId: null,
+ dynamicsEnabled: false,
+ differentialEquationId: null,
+ x: 1,
+ y: 2,
+ });
+ expect(result.transitions[0]).toEqual({
+ id: "t1",
+ name: "T1",
+ inputArcs: [{ placeId: "p1", weight: 1, type: "standard" }],
+ outputArcs: [{ placeId: "p1", weight: 1 }],
+ lambdaType: "predicate",
+ lambdaCode: "",
+ transitionKernelCode: "",
+ x: 3,
+ y: 4,
+ });
+ expect(result.types).toEqual([]);
+ expect(result.parameters).toEqual([]);
+ expect(result.differentialEquations).toEqual([]);
+ });
+
+ it("omits optional keys that are absent so structural equality is preserved", () => {
+ const result = normalizeSDCPN({
+ places: [{ id: "p1", name: "P1", x: 0, y: 0 }],
+ transitions: [],
+ });
+
+ expect(Object.hasOwn(result.places[0]!, "visualizerCode")).toBe(false);
+ expect(Object.hasOwn(result.places[0]!, "showAsInitialState")).toBe(false);
+ expect(Object.hasOwn(result, "scenarios")).toBe(false);
+ expect(Object.hasOwn(result, "metrics")).toBe(false);
+ });
+
+ it("preserves provided extension values instead of overwriting them", () => {
+ const result = normalizeSDCPN({
+ places: [
+ {
+ id: "p1",
+ name: "P1",
+ x: 0,
+ y: 0,
+ colorId: "c1",
+ dynamicsEnabled: true,
+ differentialEquationId: "d1",
+ visualizerCode: "code",
+ showAsInitialState: true,
+ },
+ ],
+ transitions: [
+ {
+ id: "t1",
+ name: "T1",
+ inputArcs: [{ placeId: "p1", weight: 2, type: "inhibitor" }],
+ outputArcs: [{ placeId: "p1", weight: 3 }],
+ x: 0,
+ y: 0,
+ lambdaType: "stochastic",
+ lambdaCode: "l",
+ transitionKernelCode: "k",
+ },
+ ],
+ });
+
+ expect(result.places[0]).toMatchObject({
+ colorId: "c1",
+ dynamicsEnabled: true,
+ differentialEquationId: "d1",
+ visualizerCode: "code",
+ showAsInitialState: true,
+ });
+ expect(result.transitions[0]).toMatchObject({
+ inputArcs: [{ placeId: "p1", weight: 2, type: "inhibitor" }],
+ outputArcs: [{ placeId: "p1", weight: 3 }],
+ lambdaType: "stochastic",
+ lambdaCode: "l",
+ transitionKernelCode: "k",
+ });
+ });
+
+ it("is idempotent on an already-complete SDCPN", () => {
+ const complete: SDCPN = {
+ places: [
+ {
+ id: "p1",
+ name: "P1",
+ colorId: null,
+ dynamicsEnabled: false,
+ differentialEquationId: null,
+ x: 0,
+ y: 0,
+ },
+ ],
+ transitions: [
+ {
+ id: "t1",
+ name: "T1",
+ inputArcs: [{ placeId: "p1", weight: 1, type: "standard" }],
+ outputArcs: [{ placeId: "p1", weight: 1 }],
+ lambdaType: "predicate",
+ lambdaCode: "",
+ transitionKernelCode: "",
+ x: 0,
+ y: 0,
+ },
+ ],
+ types: [],
+ parameters: [],
+ differentialEquations: [],
+ };
+
+ expect(isSDCPNEqual(normalizeSDCPN(complete), complete)).toBe(true);
+ });
+});
diff --git a/libs/@hashintel/petrinaut-core/src/types/sdcpn-input.ts b/libs/@hashintel/petrinaut-core/src/types/sdcpn-input.ts
new file mode 100644
index 00000000000..e8eb057cf78
--- /dev/null
+++ b/libs/@hashintel/petrinaut-core/src/types/sdcpn-input.ts
@@ -0,0 +1,147 @@
+import type {
+ Color,
+ DifferentialEquation,
+ ID,
+ InputArcType,
+ Metric,
+ Parameter,
+ Scenario,
+ SDCPN,
+} from "./sdcpn";
+
+/**
+ * Loose, authoring-friendly variant of {@link SDCPN}.
+ *
+ * The canonical {@link SDCPN} type is what the editor, simulation, LSP, and
+ * serialization consume, so every extension field is present on it. When a
+ * host integrates Petrinaut and maps its own domain model into a Petri net, it
+ * usually doesn't care about colours, dynamics, stochasticity, or parameters —
+ * yet the strict type still forces it to spell out `colorId: null`,
+ * `lambdaCode: ""`, empty `types`/`parameters`/`differentialEquations` arrays,
+ * and so on for every node.
+ *
+ * `SDCPNInput` makes all of that optional. Pass it to
+ * {@link normalizeSDCPN} (or straight to `createJsonDocHandle`, which
+ * normalizes internally) to get a fully-populated {@link SDCPN} with plain-net
+ * defaults filled in. A complete {@link SDCPN} is always a valid `SDCPNInput`,
+ * so existing callers are unaffected.
+ */
+export type SDCPNInput = {
+ places: SDCPNPlaceInput[];
+ transitions: SDCPNTransitionInput[];
+ /** @default [] */
+ types?: Color[];
+ /** @default [] */
+ parameters?: Parameter[];
+ /** @default [] */
+ differentialEquations?: DifferentialEquation[];
+ scenarios?: Scenario[];
+ metrics?: Metric[];
+};
+
+export type SDCPNPlaceInput = {
+ id: ID;
+ name: string;
+ x: number;
+ y: number;
+ /** @default null */
+ colorId?: ID | null;
+ /** @default false */
+ dynamicsEnabled?: boolean;
+ /** @default null */
+ differentialEquationId?: ID | null;
+ visualizerCode?: string;
+ showAsInitialState?: boolean;
+};
+
+export type SDCPNInputArcInput = {
+ placeId: string;
+ /** @default 1 */
+ weight?: number;
+ /** @default "standard" */
+ type?: InputArcType;
+};
+
+export type SDCPNOutputArcInput = {
+ placeId: string;
+ /** @default 1 */
+ weight?: number;
+};
+
+export type SDCPNTransitionInput = {
+ id: ID;
+ name: string;
+ inputArcs: SDCPNInputArcInput[];
+ outputArcs: SDCPNOutputArcInput[];
+ x: number;
+ y: number;
+ /** @default "predicate" */
+ lambdaType?: "predicate" | "stochastic";
+ /** @default "" */
+ lambdaCode?: string;
+ /** @default "" */
+ transitionKernelCode?: string;
+};
+
+/**
+ * Fill plain-net defaults into an {@link SDCPNInput} to produce a canonical
+ * {@link SDCPN}. Idempotent: normalizing an already-complete `SDCPN` returns an
+ * equivalent value.
+ *
+ * Optional output fields (`visualizerCode`, `showAsInitialState`, `scenarios`,
+ * `metrics`) are only set when present on the input, so the result matches the
+ * shape the editor itself produces (relevant for structural dirty-tracking via
+ * `isSDCPNEqual`).
+ */
+export function normalizeSDCPN(input: SDCPNInput): SDCPN {
+ const result: SDCPN = {
+ places: input.places.map((place) => {
+ const normalized: SDCPN["places"][number] = {
+ id: place.id,
+ name: place.name,
+ colorId: place.colorId ?? null,
+ dynamicsEnabled: place.dynamicsEnabled ?? false,
+ differentialEquationId: place.differentialEquationId ?? null,
+ x: place.x,
+ y: place.y,
+ };
+ if (place.visualizerCode !== undefined) {
+ normalized.visualizerCode = place.visualizerCode;
+ }
+ if (place.showAsInitialState !== undefined) {
+ normalized.showAsInitialState = place.showAsInitialState;
+ }
+ return normalized;
+ }),
+ transitions: input.transitions.map((transition) => ({
+ id: transition.id,
+ name: transition.name,
+ inputArcs: transition.inputArcs.map((arc) => ({
+ placeId: arc.placeId,
+ weight: arc.weight ?? 1,
+ type: arc.type ?? "standard",
+ })),
+ outputArcs: transition.outputArcs.map((arc) => ({
+ placeId: arc.placeId,
+ weight: arc.weight ?? 1,
+ })),
+ lambdaType: transition.lambdaType ?? "predicate",
+ lambdaCode: transition.lambdaCode ?? "",
+ transitionKernelCode: transition.transitionKernelCode ?? "",
+ x: transition.x,
+ y: transition.y,
+ })),
+ types: input.types ?? [],
+ parameters: input.parameters ?? [],
+ differentialEquations: input.differentialEquations ?? [],
+ };
+
+ if (input.scenarios !== undefined) {
+ result.scenarios = input.scenarios;
+ }
+ if (input.metrics !== undefined) {
+ result.metrics = input.metrics;
+ }
+
+ return result;
+}
diff --git a/libs/@hashintel/petrinaut/INTEGRATION.md b/libs/@hashintel/petrinaut/INTEGRATION.md
new file mode 100644
index 00000000000..ec326163ff8
--- /dev/null
+++ b/libs/@hashintel/petrinaut/INTEGRATION.md
@@ -0,0 +1,301 @@
+# Petrinaut Integration Guide
+
+This guide covers embedding the `@hashintel/petrinaut` editor in another React
+application where the host application owns the Petri net data.
+
+The main integration point is a `PetrinautDocHandle`. The handle is the bridge
+between your store and the editor:
+
+```mermaid
+flowchart LR
+ Store["Host app
source of truth"]
+ Handle["PetrinautDocHandle"]
+ Editor["<Petrinaut />"]
+
+ Store -- "toSDCPN()" --> Handle
+ Handle -- "doc() / subscribe()" --> Editor
+ Editor -- "change(fn)" --> Handle
+ Handle -- "fromSDCPN()" --> Store
+```
+
+The interface is experimental and may change. The required pieces today are:
+
+```ts
+import type {
+ DocChangeEvent,
+ DocHandleState,
+ PetrinautDocHandle,
+ ReadableStore,
+ SDCPN,
+} from "@hashintel/petrinaut-core";
+
+interface PetrinautDocHandle {
+ readonly id: string;
+ readonly state: ReadableStore;
+ whenReady(): Promise;
+ doc(): SDCPN | undefined;
+ change(fn: (draft: SDCPN) => void): void;
+ subscribe(listener: (event: DocChangeEvent) => void): () => void;
+}
+```
+
+`state` can be a constant `"ready"` store for synchronous integrations.
+`whenReady()` can return a resolved promise. Add `capabilities` when the host
+wants to restrict extensions, and add `history` only if the handle can support
+undo and redo.
+
+## Map Your Model To SDCPN
+
+Petrinaut edits `SDCPN` documents. External applications usually have their own
+model, so start by writing mapping functions.
+
+Use `SDCPNInput` when creating documents from a simple host model. It is a
+looser authoring shape: extension fields can be omitted and filled with
+plain-net defaults by `normalizeSDCPN()` or `createJsonDocHandle()`.
+
+```ts
+import type { SDCPNInput } from "@hashintel/petrinaut-core";
+
+type WorkflowGraph = {
+ id: string;
+ title: string;
+ nodes: WorkflowNode[];
+ edges: WorkflowEdge[];
+};
+
+function toSDCPN(graph: WorkflowGraph): SDCPNInput {
+ return {
+ places: graph.nodes.map((node) => ({
+ id: node.id,
+ name: node.label,
+ x: node.x,
+ y: node.y,
+ showAsInitialState: node.isInitial,
+ })),
+ transitions: graph.edges.map((edge) => ({
+ id: edge.id,
+ name: edge.label,
+ inputArcs: [{ placeId: edge.sourceId }],
+ outputArcs: [{ placeId: edge.targetId }],
+ x: edge.x ?? 0,
+ y: edge.y ?? 0,
+ })),
+ };
+}
+```
+
+Mandatory fields for plain nets:
+
+| Entity | Required fields |
+| --- | --- |
+| Place | `id`, `name`, `x`, `y` |
+| Transition | `id`, `name`, `inputArcs`, `outputArcs`, `x`, `y` |
+| Arc | `placeId` |
+
+Defaulted fields include arc `weight: 1`, input arc `type: "standard"`,
+place `colorId: null`, `dynamicsEnabled: false`, transition
+`lambdaType: "predicate"`, empty lambda/kernel code, and empty top-level
+`types`, `parameters`, and `differentialEquations` arrays.
+
+The reverse mapping is application-specific:
+
+```ts
+import type { SDCPN } from "@hashintel/petrinaut-core";
+
+function fromSDCPN(sdcpn: SDCPN, baseGraph: WorkflowGraph): WorkflowGraph {
+ return {
+ ...baseGraph,
+ nodes: sdcpn.places.map((place) => ({
+ id: place.id,
+ label: place.name,
+ x: place.x,
+ y: place.y,
+ isInitial: place.showAsInitialState ?? false,
+ })),
+ edges: sdcpn.transitions.map((transition) => ({
+ id: transition.id,
+ label: transition.name,
+ sourceId: transition.inputArcs[0]?.placeId ?? "",
+ targetId: transition.outputArcs[0]?.placeId ?? "",
+ })),
+ };
+}
+```
+
+## Choose A Handle Pattern
+
+Use `createJsonDocHandle()` when Petrinaut can own an in-memory document:
+
+```ts
+import { createJsonDocHandle } from "@hashintel/petrinaut-core";
+
+const handle = createJsonDocHandle({
+ id: graph.id,
+ initial: toSDCPN(graph),
+ capabilities: {
+ disabledExtensions: ["colors", "stochasticity", "dynamics", "parameters"],
+ },
+});
+```
+
+This gives you patch events, extension sanitization, and undo/redo history. It
+is the best fit for demos, local editing, and hosts that only need to seed the
+editor.
+
+If your application is the source of truth, implement a small adapter handle
+instead. That keeps editor-originated edits (`source: "local"`) separate from
+host-originated updates (`source: "remote"`), which matters for collaboration,
+history, and avoiding echo loops.
+
+```ts
+import { produce } from "immer";
+import {
+ isSDCPNEqual,
+ normalizeSDCPN,
+ type DocChangeEvent,
+ type DocHandleState,
+ type PetrinautDocHandle,
+ type ReadableStore,
+ type SDCPN,
+} from "@hashintel/petrinaut-core";
+
+type WorkflowHandle = PetrinautDocHandle & {
+ applyExternal(graph: WorkflowGraph): void;
+};
+
+function readyStore(): ReadableStore {
+ return {
+ get: () => "ready",
+ subscribe: () => () => {},
+ };
+}
+
+function createWorkflowHandle(
+ initialGraph: WorkflowGraph,
+ onGraphChange: (graph: WorkflowGraph) => void,
+): WorkflowHandle {
+ let current: SDCPN = normalizeSDCPN(toSDCPN(initialGraph));
+ let latestGraph = initialGraph;
+ const subscribers = new Set<(event: DocChangeEvent) => void>();
+
+ const emit = (event: DocChangeEvent) => {
+ for (const subscriber of subscribers) {
+ subscriber(event);
+ }
+ };
+
+ return {
+ id: initialGraph.id,
+ state: readyStore(),
+ whenReady: () => Promise.resolve(),
+ doc: () => current,
+ change(fn) {
+ const next = produce(current, fn);
+ if (next === current) {
+ return;
+ }
+
+ current = next;
+ latestGraph = fromSDCPN(current, latestGraph);
+ emit({ next: current, source: "local" });
+ onGraphChange(latestGraph);
+ },
+ subscribe(listener) {
+ subscribers.add(listener);
+ return () => {
+ subscribers.delete(listener);
+ };
+ },
+ applyExternal(graph) {
+ const next = normalizeSDCPN(toSDCPN(graph));
+ if (isSDCPNEqual(next, current)) {
+ return;
+ }
+
+ current = next;
+ latestGraph = graph;
+ emit({ next: current, source: "remote" });
+ },
+ };
+}
+```
+
+## Mount The Editor
+
+Keep one handle per host document. Recreate the handle when switching to a
+different document, and push later host-store updates through your adapter.
+
+```tsx
+import { Petrinaut } from "@hashintel/petrinaut";
+import { useEffect, useMemo, useRef } from "react";
+
+export function WorkflowEditor({
+ graph,
+ onGraphChange,
+}: {
+ graph: WorkflowGraph;
+ onGraphChange: (graph: WorkflowGraph) => void;
+}) {
+ const handle = useMemo(
+ () => createWorkflowHandle(graph, onGraphChange),
+ [graph.id, onGraphChange],
+ );
+ const seededRef = useRef(false);
+
+ useEffect(() => {
+ if (!seededRef.current) {
+ seededRef.current = true;
+ return;
+ }
+ handle.applyExternal(graph);
+ }, [graph, handle]);
+
+ return (
+ onGraphChange({ ...graph, title })}
+ hideNetManagementControls="except-title"
+ />
+ );
+}
+```
+
+Key props:
+
+| Prop | Purpose |
+| --- | --- |
+| `handle` | Required document handle |
+| `title` / `setTitle` | Host-owned title shown in the top bar |
+| `readonly` | Disable all editing |
+| `hideNetManagementControls` | Hide New/Open/Import controls; use `"except-title"` to keep the title |
+| `existingNets`, `createNewNet`, `loadPetriNet` | Optional multi-net management hooks |
+| `aiAssistant`, `viewportActions`, `slots` | Optional host extension points |
+| Worker factories | Optional simulation, Monte Carlo, and language-server worker factories |
+
+## Capabilities
+
+Pass handle capabilities to restrict editor features. Disabled extension data is
+hidden from the UI and stripped from the document.
+
+```ts
+import type { PetrinautHandleCapabilities } from "@hashintel/petrinaut-core";
+
+const capabilities: PetrinautHandleCapabilities = {
+ disabledExtensions: ["colors", "stochasticity", "dynamics", "parameters"],
+};
+```
+
+Available extensions are `"colors"`, `"stochasticity"`, `"dynamics"`, and
+`"parameters"`. Disabling `"colors"` automatically disables `"dynamics"`.
+
+## Reference
+
+| What | Location |
+| --- | --- |
+| Visual editor props | `libs/@hashintel/petrinaut/src/ui/petrinaut.tsx` |
+| In-memory handle | `libs/@hashintel/petrinaut-core/src/handle/json-doc-handle/create-json-doc-handle.ts` |
+| Handle interface | `libs/@hashintel/petrinaut-core/src/handle/types.ts` |
+| SDCPN types | `libs/@hashintel/petrinaut-core/src/types/sdcpn.ts` |
+| SDCPN input helpers | `libs/@hashintel/petrinaut-core/src/types/sdcpn-input.ts` |
+| Storybook multi-net pattern | `libs/@hashintel/petrinaut/src/ui/petrinaut-story-provider.tsx` |
diff --git a/libs/@hashintel/petrinaut/README.md b/libs/@hashintel/petrinaut/README.md
index f7c5d8a2f33..c6cddd3a70d 100644
--- a/libs/@hashintel/petrinaut/README.md
+++ b/libs/@hashintel/petrinaut/README.md
@@ -4,6 +4,28 @@ A component for editing [**Petri nets**](https://en.wikipedia.org/wiki/Petri_net
Currently **under development** and not ready for usage.
+## Embedding Petrinaut
+
+The visual editor is exposed as a React component:
+
+```tsx
+import { Petrinaut } from "@hashintel/petrinaut";
+import { createJsonDocHandle } from "@hashintel/petrinaut-core";
+
+const handle = createJsonDocHandle({
+ id: "my-net",
+ initial: { places: [], transitions: [] },
+});
+
+export function App() {
+ return ;
+}
+```
+
+For host applications that own their Petri net data, implement a
+`PetrinautDocHandle` adapter and pass it to ``. See
+[INTEGRATION.md](INTEGRATION.md) for the recommended patterns.
+
## Development Mode
For development and testing, you can use the included dev mode:
diff --git a/libs/@hashintel/petrinaut/package.json b/libs/@hashintel/petrinaut/package.json
index 236513bf3e5..786b4f72816 100644
--- a/libs/@hashintel/petrinaut/package.json
+++ b/libs/@hashintel/petrinaut/package.json
@@ -12,6 +12,7 @@
"files": [
"dist",
"CHANGELOG.md",
+ "INTEGRATION.md",
"LICENSE",
"LICENSE-APACHE.md",
"LICENSE-MIT.md",