Zum Inhalt springen
Developer Preview — APIs and language features may change before 1.0

Custom Patterns

Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.

Patterns are machines. A pattern machine accepts data and produces a component tree. The component tree renders as a web page, an embedded widget, a mobile screen, or a CLI table. You write one pattern; it works everywhere.

Your first pattern

A pattern machine is a regular mashinTalk machine with a specific contract: it accepts data and responds with a components list.

machine @myorg/patterns/metric-card
achieves
Render a single metric value with label, formatting, and trend indicator.
accepts
value as number, required
label as text, required
format as text
trend_direction as text
trend_value as number
responds with
components as list
implements
compute render
{components: [
{type: "metric-card",
value: input.value,
label: input.label,
format: input.format || "number",
trend: {direction: input.trend_direction, value: input.trend_value}}
]}
verifies
test "renders with label and value"
given {value: 42, label: "Users"}
expect {components: [{type: "metric-card", value: 42, label: "Users"}]}

Using a pattern

Reference your pattern in a page’s render declaration:

machine dashboard
responds with
active_users as number
revenue as number
implements
compute fetch
{active_users: 1234, revenue: 56789}
expresses
page
title: "Dashboard"
render active_users as @myorg/patterns/metric-card
label: "Active Users"
render revenue as @myorg/patterns/metric-card
label: "Revenue"
format: currency

The page calls your pattern machine twice, once for each field, and renders both component trees.

The component tree

Pattern machines produce JSON arrays of atomic component specs. There are ~20 component types that map to platform widgets:

Component typeWhat it renders
textParagraph text
headingSection heading (level 1-6)
markdownRendered markdown content
codeSyntax-highlighted code block
imageImage with alt text
dividerHorizontal rule
tableData table with rows and columns
listOrdered or unordered list
key-valueLabeled key-value pairs
json-treeExpandable JSON viewer
metric-cardSingle metric with trend
sparklineInline mini chart
chartLine, bar, or pie chart
stat-groupGrid of metrics
formInput form with fields
buttonAction button
button-groupRow of buttons
alertInfo, warning, error, success message
progressProgress bar
spinnerLoading indicator

Your pattern composes these types. The renderer handles the pixels. You never write HTML, CSS, or framework code.

Publishing a pattern

Publish to the Kura registry like any krate:

Terminal window
mashin publish

Anyone can install it:

Terminal window
mashin install @myorg/patterns/metric-card

And use it in their machines:

render revenue as @myorg/patterns/metric-card

No JavaScript. No npm. No frontend rebuild. The pattern is a machine.

AI and patterns

Pattern machines are visible to the LLM via me.patterns:

me.patterns = [
{name: "@myorg/patterns/metric-card",
description: "Single metric with trend indicator.",
accepts: {value: "number, required", label: "text", format: "text", ...}},
...
]

When you ask Koda “add a chart to my dashboard,” Koda reads the pattern catalog, selects a pattern whose accepts contract matches your output data, and adds the render declaration. If no pattern fits, Koda can create a new one as a machine.

Composing patterns

Patterns can call other patterns:

machine @myorg/patterns/dashboard-section
accepts
title as text, required
metric_value as number
table_rows as list
responds with
components as list
implements
ask metric, from: @mashin/patterns/metric-card
value: input.metric_value
label: input.title
ask table, from: @mashin/patterns/data-table
rows: input.table_rows
compute compose
{components:
[{type: "heading", text: input.title, level: 2}]
++ steps.metric.components
++ steps.table.components
}

A pattern that uses two other patterns. The composition is governed: each pattern call is a machine execution with a ledger entry.

Testing patterns

Pattern machines use the same verifies section as any machine:

verifies
test "renders empty table for no rows"
given {rows: [], columns: ["a", "b"]}
expect {components: [{type: "table", rows: []}]}
test "auto-derives columns"
given {rows: [{x: 1, y: 2}]}
expect {components: [{type: "table", columns: ["x", "y"]}]}

Run tests: mashin test my_pattern.mashin

Adding renderers for new frameworks

The component tree is JSON. Any framework that can render JSON component specs can render mashin pages.

Writing a renderer

A renderer maps component types to framework-native widgets. For example, a Vue renderer:

vue-mashin-renderer.js
const componentMap = {
'text': (props) => h('p', props.text),
'heading': (props) => h(`h${props.level}`, props.text),
'table': (props) => h(VueTable, { rows: props.rows, columns: props.columns }),
'metric-card': (props) => h(VueMetricCard, { value: props.value, label: props.label }),
// ... map all ~20 types
};
export function renderComponentTree(components) {
return components.map(spec => {
const render = componentMap[spec.type];
return render ? render(spec) : h('div', `[Unknown: ${spec.type}]`);
});
}

Server-side HTML renderer

For Rails, Phoenix, Go, or any server-rendered framework:

app/helpers/mashin_helper.rb
def render_mashin_component(spec)
case spec["type"]
when "text"
content_tag(:p, spec["text"])
when "heading"
content_tag(:"h#{spec['level']}", spec["text"])
when "table"
render partial: "mashin/table", locals: { rows: spec["rows"], columns: spec["columns"] }
when "metric-card"
render partial: "mashin/metric_card", locals: { value: spec["value"], label: spec["label"] }
end
end
lib/my_app_web/components/mashin_components.ex
def mashin_component(%{type: "text"} = spec) do
~H|<p><%= spec["text"] %></p>|
end
def mashin_component(%{type: "table"} = spec) do
~H|<table>...</table>|
end
templates/mashin_component.go
func renderComponent(spec map[string]interface{}) template.HTML {
switch spec["type"] {
case "text":
return template.HTML(fmt.Sprintf("<p>%s</p>", spec["text"]))
case "table":
return renderTable(spec)
}
}

WASM renderer

For high-performance client-side rendering:

// rust wasm renderer
#[wasm_bindgen]
pub fn render_component_tree(json: &str) -> String {
let components: Vec<ComponentSpec> = serde_json::from_str(json).unwrap();
let mut html = String::new();
for spec in components {
match spec.component_type.as_str() {
"text" => html.push_str(&format!("<p>{}</p>", spec.text)),
"table" => html.push_str(&render_table(&spec)),
_ => html.push_str(&format!("<div>[Unknown: {}]</div>", spec.component_type)),
}
}
html
}

Native renderer

For iOS (SwiftUI) or Android (Compose):

MashinRenderer.swift
struct MashinComponentView: View {
let spec: ComponentSpec
var body: some View {
switch spec.type {
case "text": Text(spec.text)
case "heading": Text(spec.text).font(.title)
case "table": MashinTable(rows: spec.rows, columns: spec.columns)
case "metric-card": MetricCard(value: spec.value, label: spec.label)
default: Text("[Unknown: \(spec.type)]")
}
}
}

The contract

Every renderer implements the same interface:

  1. Fetch the component tree (JSON) from the mashin API
  2. Map each component type to a native widget
  3. Handle events by POSTing back to the mashin API
  4. Subscribe to WebSocket for real-time updates (optional)

The component types never change. New patterns produce the same types in new arrangements. Your renderer works with every pattern, including ones that haven’t been written yet.

Governance

Every pattern machine execution is governed:

  • The behavioral ledger records the render (who, when, what data)
  • The ensures section applies (a pattern that is not allowed to show PII will redact)
  • Cost is tracked (patterns with :reason steps record LLM token usage)
  • The evolution ledger versions pattern changes

A pattern is not “just rendering.” It is a governed transformation of data into a visual interface. The same governance that protects machine execution protects rendering.

See also