DRYL COMPONENTS
Color mode: System — switch to light
2.20.1
Intelligence

DrylCanvas

AI-aware

DrylCanvas renders a CanvasSpec — a titled tree of curated DRYL components — as a live surface. It is deliberately dumb: a spec goes in, interactions come out. Where the spec came from is nobody's business but the host's: code, a database, a saved document, or an AI generation (that last one is DrylAiCanvas, which wraps this component).

Data binding

A node can carry a data binding instead of literal values: { "source": "sales.byMonth", "params": { … }, "refresh": "interval:30s" }. The host registers named sources; the canvas resolves them, dedupes identical source-plus-params into a single call, and refreshes without asking anyone.

The result is that numbers stop being prose. An AI-authored artifact no longer has to write them into its brief — it references a source, the model never sees a single row, and the values stay current long after the generation finished.

Every refresh is a movement, not a rebuild. The first load shows a skeleton; after that the node keeps its identity and a changed value arrives as the same change-pulse and count-up an AI patch produces. A refresh that changes nothing renders nothing at all — a dashboard on a 30-second interval must not blink.

A dashboard bound to host data

Sales
Revenue
€1,260k
Open orders
12
Revenue per month
No. Customer Region Value
4702 Acme Ltd North 8,400
4705 Globex Inc South 12,900
4708 Initech LLC North 3,150
Changes the underlying data and calls Invalidate — the bound nodes move, they are not rebuilt.

Registering a source

A source is a name, one sentence for the model, and a handler. Its parameters are a C# record — the record is the schema, so there is no prose description to keep in step and an unsupported parameter type fails at startup rather than when the model happens to find it. Tenant and user come from the handler's DI scope, never from the spec, so a generated artifact cannot reach data it should not see.

csharp
public sealed record SalesParams(int Year, string? Region = null);

builder.Services.AddDrylCanvasDataSource("sales.byMonth",
    "Revenue per month in €k.",
    async (SalesParams p, CanvasDataContext ctx, CancellationToken ct) =>
    {
        var db = ctx.Services.GetRequiredService<AppDb>();
        var rows = await db.SalesAsync(p.Year, p.Region, ct);
        return CanvasData.Series(rows.Select(r => r.Month), ("Revenue", rows.Select(r => r.Total)));
    });

// A source with no parameters at all.
builder.Services.AddDrylCanvasDataSource("orders.open", "The open orders.",
    (CanvasDataContext ctx, CancellationToken ct) =>
        Task.FromResult(CanvasData.Rows(columns, rows)));

Refreshing

Three ways, all of them the host's or the user's call — never the model's: the declarative refresh: "interval:<n>s" (one timer per canvas, five-second floor), the button that appears in the header as soon as the artifact has a binding, and ICanvasDataService.Invalidate when the host knows something moved. A { "$field": "region" } parameter adds a fourth path for free: a select at the top of the artifact, and the nodes that depend on it follow — debounced, and without a model turn.

csharp
@inject ICanvasDataService Data

// Everything bound to this source reloads …
Data.Invalidate("orders.open");

// … or only the bindings whose parameters match.
Data.Invalidate("sales.byMonth", new { year = 2026, region = "North" });

Actions — the write side

Data binding lets an artifact read. An action lets it write: a button binds to a registered host command instead of turning the click into a chat message. The handler is ordinary C# with a DI scope and typed arguments, and its result tells the canvas what to do next — show a message, reload the affected sources, patch a node.

The AI builds the button. It never presses it. There is no run_action tool and no path from a model output to a handler; the only caller is the click. That is a property of the architecture, not a promise in a prompt — and it is why an artifact may safely offer "Release order" at all.

Buttons wired to host commands

Order release
Open orders
12
Revenue
€1,260k
No. Customer Region Value
4702 Acme Ltd North 8,400
4705 Globex Inc South 12,900
4708 Initech LLC North 3,150

The catalog

Thirty node types, all of them mapped onto ordinary DRYL components — a spec can never render anything the library does not already ship. The ones a real line-of-business screen reaches for first:

dataGrid is the interactive big brother of table: bind it to a rows source and it sorts, filters, searches and pages, up to a thousand rows. table stays for the small static case. kpi is a compact row of stats, list and keyValue take the same rows sources (a keyValue wants exactly two columns), and accordion folds detail away the way tabs puts it side by side. code, image and emptyState cover the remaining content shapes — an artifact with nothing to show says so, rather than rendering an empty card.

form is the one that changes how you write an artifact. It is a container whose action sits on the container itself: the interactive nodes inside it become one command with one submit button, and required fields are checked before the handler is ever called. A generated screen no longer needs a button per field — and the guarantee from actions still holds, because submitting is still something only a person does.

An order cockpit

Order cockpit
Quarterly target
€1.2M
Service level
98.4%+0.6%
Open claims
3-2
No.
Customer
Region
Value
4702Acme LtdNorth8,400
4705Globex IncSouth12,900
4708Initech LLCNorth3,150
Order
4702
Customer
Acme Ltd
Region
North
Value
€8,400
Type a customer and submit — the grid, both key/value sections and the KPI row all move on the one command. Submit with the field empty to see the required hint.

Registering an action

Same shape as a source, deliberately: a name, one sentence for the model, and a handler whose arguments are a C# record. A { "$field": "order" } argument reads the live value of an interactive node, so the button acts on what the user selected.

csharp
public sealed record ApproveArgs(string OrderId, string? Note = null);

builder.Services.AddDrylCanvasAction("order.approve",
    "Releases an order.",
    async (ApproveArgs a, CanvasActionContext ctx, CancellationToken ct) =>
    {
        await ctx.Services.GetRequiredService<IOrderService>().ApproveAsync(a.OrderId, ct);
        return CanvasActionResult.Ok("Order released")
                                 .Refresh("orders.open", "orders.openCount");
    });

// In the artifact, next to "props" — never inside them:
// { "id": "approve", "type": "button",
//   "props": { "label": "Release", "kind": "danger" },
//   "action": { "name": "order.approve",
//               "args": { "orderId": { "$field": "order" } },
//               "confirm": "Really release this order?" } }
//
// On a form the action sits on the container, and the submit button is implicit:
// { "id": "create", "type": "form",
//   "props": { "submitLabel": "Create order", "required": ["customer"] },
//   "action": { "name": "order.create",
//               "args": { "customer": { "$field": "customer" } } },
//   "children": [ /* inputText, select, … */ ] }

Success is a toast — the artifact refreshes visibly anyway, so the message may pass. A failure stays inline at the button, because it asks the user to do something and must not expire. confirm puts a DrylDialog in front of the handler; without a DrylDialogProvider the action is refused rather than run unconfirmed. AskAi(…) is opt-in and reaches your existing OnInteraction wiring unchanged.

An unhandled error has occurred. Reload x

Reconnecting to the server…

Attempt /

Next try in s