A Vessel is Maelstrom’s representation of a UI component — a definition that wraps your React component and gives the Engine a typed manifest of its Variants, sizes, and Actions. This guide shows you how to author one, from definition to render.
Use with a coding agent
For a focused exercise, give an agent the Vessel modeling prompt and point it at an existing capability. It is intended to preserve that capability’s product behavior while shaping it as a Vessel. If you are assessing a broader React integration, the React inventory prompt is read-only and proposes a plan; the separate React conversion prompt is for implementing an already approved plan, not for granting new scope.
The Vessel modeling skill is the reusable workflow for this task; the bundle also includes a React migration lead skill.
Model a capability as a Vessel
Model this existing capability as a Maelstrom Vessel without changing its product
behavior.
Capability/component: [PATH]
Repository root: [PATH]
Goal: [USER GOAL]
Inspect before editing: the component and its callers, state/data hooks or stores,
API handlers, types, and relevant tests. Read the installed SDK's source/types and
record the exact installed `@maelstrom-co/*` versions before selecting APIs. Treat
the Vessel definition and its descriptions as runtime behavior contracts.
## Decide whether to expose it
Inventory relevant user-visible product capabilities. For each, cite observed
behavior and a plausible user intent, explain what the Engine gains by selecting
and presenting it as a unit, and decide whether to expose it or keep it internal.
Do not turn every React component into a Vessel. Keep buttons, badges, spinners,
and incidental implementation state inside their capability. A simple useful
capability may be a single-Variant Vessel with no Actions. Reject invented states
or Actions even if they would make a more elaborate example.
For a selected candidate, produce an observed behavior table:
| UI state or operation | Source evidence | Props/data | Side effects |
| --- | --- | --- | --- |
Use code references precise enough for another developer to check. If evidence is
missing or behavior is ambiguous, report the gap instead of filling it in.
## Model, then implement only when authorized
First produce the evidence-backed inventory and Vessel proposal without editing.
Implementation is authorized only when the task explicitly requests applying the
proposal to this repository. If that authorization is absent, make zero edits and
return the proposal and bounded implementation plan. If authorized, implement
only the selected capability and necessary registration/integration seams.
1. Define Variants only for meaningfully distinct, legal, user-visible states
supported by observed behavior. Record the evidence for each boundary.
2. Define Actions only for existing legal operations available in that active
Variant. For each, identify parameters, application-owned effects, any real
target Variant, and duplicate/retry risk.
3. Treat descriptions as product-language contracts: explain the capability and
when it is useful, not the component's implementation. Preserve the existing
component/rendering where practical and use the installed SDK's typed Vessel
props/APIs.
4. Keep business and API effects in application-owned handlers. Preserve existing
validation and confirmation behavior. Do not mark an accumulating, destructive,
or otherwise side-effecting operation idempotent without evidence that the
underlying operation is genuinely idempotent. Do not use casts to conceal an
invalid definition.
5. When implementation is authorized, change only the selected capability and
necessary registration/integration seams. Do not broaden into an
application-wide migration.
## Verify and report
Add or update tests for the selected Variant rendering and each exposed Action
boundary and handler effect. Run relevant typecheck, tests, and lint/format checks.
Report exact commands and results; do not claim unrun checks passed.
Return the capability inventory and decision, evidence-backed state/operation
table, final Vessel contract, changed files, verification results, side-effect and
idempotence rationale, and any behavior that could not be mapped safely.
Plan a React integration
Create an evidence-backed plan for integrating Maelstrom into an existing React
application for this goal:
[PRODUCT GOAL]
Repository root: [PATH]
This is a **read-only planning task**. Inspect, reason, and report; make zero
edits. Do not create, modify, format, or delete files, run generators, install
packages, or apply the proposed integration. If the requested goal cannot be
planned safely from available evidence, report what is missing without changing
the repository.
## Inspect the application and SDK
1. Read workspace/package manifests and lockfile, application entry and root,
routing, relevant UI, state/data/network code, server boundaries, and tests.
Record the installed versions of every relevant `@maelstrom-co/*` package;
distinguish declared ranges from resolved versions.
2. Inspect source, types, and tests for those installed versions. Establish the
actual APIs and supported behavior from that evidence. Do not rely on samples,
memory, or APIs from another release. Record incompatibilities or unavailable
packages; do not install or upgrade anything.
3. Trace each capability being considered from rendered interface through its
data owner and handlers to any external effects. Cite repository paths and
symbols/lines where practical. Mark conclusions as observed, inferred, or
unknown.
## Inventory and choose a pilot
For each relevant user-visible capability, report:
| Capability and observed behavior | Source evidence | Plausible user intent | Engine value as a unit | Decision |
| --- | --- | --- | --- | --- |
Decide whether each candidate belongs in the Chart as a Vessel or should remain
an internal component. Engine value means that selection and placement by intent
can help the user; a React component's existence alone is not a reason to expose
it. Keep controls, badges, spinners, and incidental state inside their capability.
Choose the smallest useful pilot, and explain why other candidates are deferred or
rejected. A useful single-Variant Vessel with no Actions is valid. Do not invent
product capabilities, state, or operations to make the proposal look richer.
For the proposed pilot, give an observed behavior/ownership table:
| State or operation | Evidence | Props/data owner | Side effects and owner |
| --- | --- | --- | --- |
Name only legal states and operations supported by evidence. If state boundaries,
selection value, or side-effect ownership are ambiguous, identify the evidence
needed rather than guessing.
## Plan, do not implement
Describe a bounded sequence of intended changes, likely files/seams, and tests,
without writing them. The plan must:
- preserve current rendering, routing, state ownership, and user-visible behavior
unless the stated goal explicitly authorizes a change;
- define Variants only for observed, meaningfully distinct legal states;
- expose Actions only for existing legal operations, in the Variants where they
are available, and keep business/API effects in application-owned handlers;
- avoid idempotence claims without evidence the underlying effect is genuinely
idempotent, and avoid inventing idempotence keys or retry behavior;
- preserve the existing Connector and authentication owner; never put provider
credentials in browser code or move provider decisions into the browser;
- keep Engine-selected capabilities developer-authored; do not propose
model-generated JSX or client-owned Engine decisions;
- avoid broad migrations, voice work, unrelated refactors, and inline layout
reconciliation;
- call out live Engine verification separately if only a stub or local test path
is available.
If implementation would require a new product, architecture, or scope decision,
mark it as a blocker for the repository owner. Do not make that decision yourself.
## Report
Return the observed application architecture and exact package versions; the
candidate inventory and expose/internal decisions; the pilot behavior and
ownership tables; API/version constraints; a bounded implementation and test
plan; likely rollback steps; and unknowns or live-service requirements. State
explicitly that no files were changed and list the read-only commands used.
Convert a React capability
Implement the bounded React-to-Maelstrom conversion authorized below:
[APPROVED INVENTORY / PLAN]
Repository root: [PATH]
Authorized goal and limits: [GOAL AND LIMITS]
You are authorized to edit only the files necessary to implement this approved
plan. First inspect the current repository and verify that the approved plan still
matches the source. If source drift, missing evidence, an API/version mismatch,
or any material product, architecture, or scope decision prevents safe execution,
stop before editing that part and report the blocker. Do not quietly reinterpret
the authorization or expand it.
## Verify and preserve the existing behavior
1. Read package manifests and lockfile; record declared and resolved versions of
all relevant `@maelstrom-co/*` dependencies. Read the installed SDK's source,
types, docs, and tests for the APIs you intend to use. Do not copy stale sample
code or assume compatibility outside evidence in this repository.
2. Inspect the selected component and callers, routes, state/data hooks or stores,
API handlers, types, and relevant tests. Record the actual rendering, state,
legal transitions, and effects before making changes.
3. Preserve existing rendering, routing, state ownership, and user-visible
behavior except changes explicitly in the authorization. Preserve the
application's existing auth/Connector ownership; never add provider
credentials to browser code or move provider decisions to the client.
## Convert only the approved capability
- Define Variants only for observed, meaningfully distinct legal user-visible
states, with evidence for each boundary. A useful single-Variant Vessel with no
Actions is valid; don't invent extra states or Actions.
- Define Actions only for existing legal operations and only where available.
Keep business and API effects in the existing application-owned handlers and
preserve validation and confirmation behavior. Do not mark accumulating,
destructive, or other side-effecting operations idempotent without evidence
the underlying operation genuinely is idempotent. Do not invent deduplication,
retry semantics, or idempotence keys.
- Use installed-version typed APIs; do not conceal an invalid definition with
casts. Preserve existing component/rendering where practical. Keep controls and
incidental implementation state inside the capability instead of exposing
each as a Vessel.
- Register only the approved developer-authored Vessel(s) and make only the
smallest authorized intent/integration change. Do not replace them with
model-generated JSX. Preserve client-owned layout state while the Engine
plans layout directives; neither the Engine nor browser rendering takes
ownership of that client state.
- Do not widen into broad migration, voice, unrelated refactors, new auth, or
inline overflow/size reconciliation.
## Verify and rollback
Add or update tests for selected Variant rendering and each exposed Action's
legal boundary and handler effect. Run relevant project typecheck, tests, and
lint/format checks; give exact commands and outcomes. Do not claim that a stub,
static check, or local test verifies live Engine behavior. If live Engine
verification needs separately configured infrastructure, say so.
Before reporting, inspect the complete diff for secrets, unrelated changes, and
behavior changes. Give a rollback path naming the precise changed files and
reversal steps. If checks fail, report the failures rather than describing them
as passed; do not broaden scope to silence them.
## Report
Return observed versions and behavior, changed files and bounded implementation,
test coverage, exact verification results, Connector/credential and state/effect
ownership review, live-service limitations, and rollback instructions. Distinguish
verified facts from unresolved assumptions.
By the end of this guide you will:
Define a greetingVessel with a single variant and typed props.
Build a React component that renders those props.
Register the entry with createMaelstrom() and mount the Chart.
Descriptions are part of the runtime contract, not decoration. The Engine reads the Vessel description, each Variant description, and each Action description to decide whether the Vessel is relevant to a user’s intent. Write them as product behavior, not implementation notes.
Use defineVessel from @maelstrom-co/protocol to describe the Vessel. It’s an identity function at runtime: it returns the input as is and exists purely for type safety. Its <const V> signature captures the literal types of your Variant names, Action names, and prop shapes. Those types flow downstream so your component and Action runners are fully typed. It also constrains the object to VesselDefinition, so mistakes are caught at the definition site rather than at runtime.
import { defineVessel } from '@maelstrom-co/protocol' ;
export const greetingVessel = defineVessel ({
description: 'Displays a personalised greeting message.' ,
minSize: { width: 3 , height: 2 },
Key fields:
id : unique, short, and stable string the Engine uses to address this Vessel.
name / description : human-readable labels the LLM reads to decide when to show the Vessel.
defaultVariant : the Variant that renders before any Action transitions the Vessel. Must be a key in variants.
variants : a map of named Variants. Each Variant carries props (the shape your component receives), minSize (in grid cells; see Grid vs Tiling for how cells map to pixels), and actions (covered in Vessel Variants & Actions ).
The props values in the definition are used only to infer the TypeScript shape. The LLM fills the actual values at runtime.
Pass the definition and the component to vessel() from @maelstrom-co/react:
import type { TypedVesselProps } from '@maelstrom-co/react' ;
import { greetingVessel } from './greeting.vessel' ;
export default function GreetingComponent ({
} : TypedVesselProps < typeof greetingVessel>) {
// variantProps is a discriminated union narrowed per variant.
// With one variant this is straightforward; narrow when you add more.
if (variantProps.variant === 'card' ) {
< div style = {{ padding: '1rem' }}>
< h2 >Hello, {variantProps.name}!</ h2 >
TypedVesselProps<V> delivers three props:
Prop
Type
Description
instanceId
InstanceId
Unique ID for this placed instance of the vessel.
variantProps
discriminated union
{ variant: K } & props for the active Variant K. Narrow on .variant to access that Variant’s typed props.
executeAction
(name, params?) => void
Fire an action; covered in Vessel Variants & Actions .
Maelstrom generates and injects instanceId automatically. You’ll need it if you want to call executeAction on a specific instance from outside the component — for example, from a toolbar or a global keyboard shortcut.
variantProps bundles the active variant name and its props into a single object. With one variant it’s always { variant: 'card', name: string }. When you add more variants it becomes a discriminated union — narrow on .variant to get the right prop types.
Combine the definition and component into an entry with vessel(), then pass it to createMaelstrom():
import { createMaelstrom, vessel } from '@maelstrom-co/react' ;
import GreetingComponent from './vessels/greeting/greeting.component' ;
import { greetingVessel } from './vessels/greeting/greeting.vessel' ;
export const greetingEntry = vessel (greetingVessel, GreetingComponent);
export const m = createMaelstrom ({
vessels: [greetingEntry],
Mount the Provider and Chart in your app shell:
import { m } from './maelstrom' ;
import { myConnector } from './connector' ;
export default function App () {
< m.Provider connector = {myConnector}>
<m.Chart /> renders vessels as the engine places them. The greeting vessel appears when the engine decides to show it. You don’t position it manually.