Skip to content

Commit 8f26609

Browse files
alex-strukclaude
andcommitted
feat(demos): deleted-lineage demo (D2) + a previewable dynamic node (D3)
D2 — a new "Deleted custom node" demo that ships ALREADY tombstoned: the seeder publishes a lineage, builds a workflow on it, then soft-deletes the lineage. The reader opens it and sees the red Deleted badge, the "(deleted dynamic node)" alert and the "Activity type not registered" error, with no destructive step and nothing consumed for whoever walks next. Its guide steps name the D-11 gap (Try/Run stay enabled) and the catalog-load timing that made me misread this check the first time. D3 — the Part-14 dynamic node is now tagged `@deterministic true` AND binds its output to a ctx key. Both were needed: without the tag the script is treated as non-deterministic and deliberately never cached, so there is nothing to preview; without the binding the widget honestly reports "this step's output isn't bound to a workflow value yet". With both, 14.9 finally shows what it promises — {"fileName":"901E42EB-7094-4B67-B7FA-FD62618D2208-WALK-14-9.PDF"}. That also sharpens D-12. The bogus "cache evicted — re-run to repopulate" message appears specifically for a node that is never cached, so the fix is to say "not cached: this node is non-deterministic" rather than to blame an eviction that never happened. 14.9 ticked. 14.8's observable halves all pass on the new demo; it stays open only on D-11's "Try disabled" criterion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent e94a202 commit 8f26609

4 files changed

Lines changed: 150 additions & 4 deletions

File tree

docs-md/workflows/FEATURE_DEMO_GUIDE.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ npm run seed:demos
3535
- [Versioning — history & revert (Part 12)](#versioning-history-revert-part-12)
3636
- [Library workflow (Part 10)](#library-workflow-part-10)
3737
- [Dynamic (custom-code) node — DYN pill & script editor (Part 14)](#dynamic-customcode-node-dyn-pill-script-editor-part-14)
38+
- [Deleted custom node — Deleted badge & catalog error (Part 14)](#deleted-custom-node-deleted-badge-catalog-error-part-14)
3839
- [🤖 AI agent chat logs](#-ai-agent-chat-logs)
3940

4041
---
@@ -215,10 +216,23 @@ npm run seed:demos
215216
1. Right-click the node → **Edit script** opens the script editor with the published TypeScript source (JSDoc `@inputs`/`@outputs` drive the ports).
216217
1. **+ New custom node** (palette) opens the authoring editor — publishing runs the jsdoc → signature → ts-check → allowlist gates (`MANUAL_TEST_PLAN.md` Part 14).
217218
1. **Delete + re-create restores the node:** delete this custom node (**Dynamic nodes** page), then **+ New custom node** and publish the *same* name — it comes back with its history continued (v2), instead of dead-ending on a reserved-slug conflict (14.14).
219+
1. **Try it (14.9):** the script is tagged `@deterministic true`, so its output is **cached** and the node shows a real **preview** after a run — an untagged (non-deterministic) script re-executes every run and is deliberately never cached, so it has no preview to show.
218220
1. ⚠️ *Executing* this node in a run additionally needs the Temporal worker started with `PLATFORM_API_KEY` (14.9).
219221

220222
---
221223

224+
## Deleted custom node — Deleted badge & catalog error (Part 14)
225+
226+
**▶ Open:** [http://localhost:3000/workflows/by-slug/demo-deleted-custom-node-deleted-badge-catalog-error-part-14/edit](http://localhost:3000/workflows/by-slug/demo-deleted-custom-node-deleted-badge-catalog-error-part-14/edit)
227+
228+
1. This workflow references a custom node whose **lineage has been deleted**. Nothing to set up — it ships in that state.
229+
1. The canvas node carries a red **Deleted** badge and its settings show *(deleted dynamic node)*.
230+
1. The validation chip reads **1 error***Activity type `dyn.demo-deleted-node` is not registered*. Give the activity catalog a moment to load: `dyn.*` types are deliberately given the benefit of the doubt while it is still fetching, so the error appears a beat after the canvas.
231+
1. ⚠️ **Known gap (D-11):** **Try** and **Run** are still enabled here even though the graph cannot run — a run fails at `dynamicNode.resolveLineage`. Whether *any* validation error should disable Try, or only errors that make the graph structurally unrunnable, is an open product decision.
232+
1. Restoring it is one step: publish a node with the **same name** from **Dynamic nodes ▸ New custom node** and the lineage comes back with its history continued (14.14).
233+
234+
---
235+
222236
## 🤖 AI agent chat logs
223237

224238
Transcripts captured from **real live runs** of the workflow agent (Azure gpt-5.4). Open the demo link — the workflow the agent built loads on the canvas **and** the agent drawer opens and **replays** the whole conversation (your prompt + every tool call it made) beside it, so you can see the result and how it was built in one place.

docs-md/workflows/MANUAL_TEST_PLAN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -606,7 +606,7 @@ One `source.api` and one `source.upload` max per workflow.
606606
- [ ] **14.8 In-canvas custom node.** Palette **Custom** section → **New custom node** modal → publish → node auto-drops as `dyn.<slug>` with a grape **DYN** badge. Right-click a `dyn.*` node → **Edit script**. **Pass:** deleted-lineage node shows a red **Deleted** badge, settings Alert, Try disabled.
607607

608608
### Execute + security
609-
- [ ] **14.9 Execute (Try).** Build `source.api → dyn.uppercase-url`, wire `document`, Save → **Try** with `{"documentUrl":"https://example.com/foo.pdf"}`. **Pass:** node goes blue→green; preview shows the uppercased URL. Publish v2 (reverse) → Try → cache miss → preview shows reversed URL.
609+
- [x] **14.9 Execute (Try).** Build `source.api → dyn.uppercase-url`, wire `document`, Save → **Try** with `{"documentUrl":"https://example.com/foo.pdf"}`. **Pass:** node goes blue→green; preview shows the uppercased URL. Publish v2 (reverse) → Try → cache miss → preview shows reversed URL.
610610
- [x] **14.10 Runtime errors.** Script `throw``errorMessage` prefixed `[DynamicNodeRuntimeError] exitCode=1 …`; timeout (>60s) / stdout >5MB / invalid JSON / missing output port each map to their typed error (truncated 2KB).
611611
- [x] **14.11 🔒 Network egress blocked.** Publish/run a node doing `await fetch("https://blocked.example.com")` with that host **not** in `DYNAMIC_NODE_ALLOW_NET`. Fast path — hit the runner directly:
612612
```bash

feature-docs/20260724-workflow-builder-spec-completion/COVERAGE_GAP_LOG.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ Create, Publish, Author, Drag, Declare); only **10 of 16 parts** carry a
2626

2727
## G1 — Demos to seed (`scripts/seed-feature-demos.mjs`)
2828

29+
> **D1–D4 shipped.** D2 is a *Deleted custom node* demo that ships already
30+
> tombstoned — red **Deleted** badge, `(deleted dynamic node)` alert and the
31+
> catalog error, with no destructive step for the reader. D3 tagged the Part-14
32+
> dynamic node `@deterministic true` **and bound its output**, which is what
33+
> makes 14.9's preview show the real value:
34+
> `{"fileName":"901E42EB-…-WALK-14-9.PDF"}`.
35+
>
2936
> **D1 + D4 shipped.** The Part-10 library demo now declares a typed signature
3037
> (input *Prepared file* `PreparedFile`, output *OCR result* `OcrResult`) and
3138
> carries **two versions**. That unblocked 7.8, 10.4, 12.4 and 12.5 in one
@@ -35,8 +42,8 @@ Create, Publish, Author, Drag, Declare); only **10 of 16 parts** carry a
3542
| # | Demo | Covers | Why it's a gap | Size |
3643
|---|---|---|---|---|
3744
| ~~**D1**~~**DONE** | **Library workflow with typed ports** — declare an input `kind: PreparedFile` and an output `kind: DocumentContent` | **7.8**, and enriches Part 10 | 7.8 is the only check for typed library ports, and no seeded library declares a `kind`. I had to author `WALK-7.8 typed ports` to walk it at all | S |
38-
| **D2** | **Workflow referencing a deleted `dyn.*` lineage**, seeded already-deleted | **14.8** (deleted half) | The plan asks you to delete a lineage yourself — destructive, and it consumes the Part-14 demo for the next walker. Seeded in the end state, the user just opens it and sees the red **DELETED** badge, the settings alert and the validator error | S |
39-
| **D3** | **Runnable dynamic node** — extend the existing Part-14 demo rather than add one | **14.9** | 14.9 says "build `source.api → dyn.uppercase-url`". The existing Part-14 demo shows the DYN pill and script editor but is not wired to run. Also the natural home for **D-12** once the preview message is fixed | M |
45+
| ~~**D2**~~**DONE** | **Workflow referencing a deleted `dyn.*` lineage**, seeded already-deleted | **14.8** (deleted half) | The plan asks you to delete a lineage yourself — destructive, and it consumes the Part-14 demo for the next walker. Seeded in the end state, the user just opens it and sees the red **DELETED** badge, the settings alert and the validator error | S |
46+
| ~~**D3**~~**DONE** | **Runnable dynamic node** — extend the existing Part-14 demo rather than add one | **14.9** | 14.9 says "build `source.api → dyn.uppercase-url`". The existing Part-14 demo shows the DYN pill and script editor but is not wired to run. Also the natural home for **D-12** once the preview message is fixed | M |
4047

4148
| ~~**D4**~~**DONE** | **Part-10 demo library needs declared ports and a second version** | **7.8**, **12.5**, **10.4** | The seeded `Demo — Library workflow (Part 10)` declares **0 inputs / 0 outputs**, so the childWorkflow signature summary renders empty — nothing to look at. It also has **only one version**, so 12.5's "pick `v2` → stamps `version:2`" cannot be walked at all. Giving this one demo typed ports and a v2 closes D1 as well | S |
4249

scripts/seed-feature-demos.mjs

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1426,6 +1426,7 @@ function demoDynamicNodeScript() {
14261426
* @workflow-node
14271427
* @name ${DYN_DEMO_NAME}
14281428
* @description Uppercases the prepared file's fileName.
1429+
* @deterministic true
14291430
* @inputs { document: { kind: "Document", required: true } }
14301431
* @outputs { result: { kind: "Artifact" } }
14311432
*/
@@ -1481,6 +1482,7 @@ function dynamicNodeConfig(name, slugName) {
14811482
blobKey: { type: "string" },
14821483
documentId: { type: "string" },
14831484
preparedFileData: { type: "object" },
1485+
uppercasedName: { type: "object" },
14841486
},
14851487
nodes: {
14861488
prep: {
@@ -1503,19 +1505,99 @@ function dynamicNodeConfig(name, slugName) {
15031505
// The dynamic-node binding walk requires the required `document`
15041506
// input to be explicitly bound (auto-wire doesn't cover dyn nodes).
15051507
inputs: [{ port: "document", ctxKey: "preparedFileData" }],
1508+
// 14.9 — bind the OUTPUT too, or the preview has nothing to read and
1509+
// honestly says so ("this step's output isn't bound to a workflow
1510+
// value yet"). With the binding AND `@deterministic true` the run
1511+
// caches the result and the widget shows the uppercased fileName.
1512+
outputs: [{ port: "result", ctxKey: "uppercasedName" }],
15061513
...pos(460, 140),
15071514
},
15081515
},
15091516
edges: [{ id: "e1", source: "prep", target: "dynNode", type: "normal" }],
15101517
};
15111518
}
15121519

1520+
/**
1521+
* D2 (14.8) — a workflow that references a dynamic node whose lineage has been
1522+
* SOFT-DELETED, seeded already in that end state.
1523+
*
1524+
* The plan asks the reader to go and delete a lineage themselves. That is
1525+
* destructive, it consumes the Part-14 demo for whoever walks next, and it
1526+
* leaves the group's catalog different from how it started. Seeding the end
1527+
* state instead means the reader just opens this and sees the failure modes.
1528+
*/
1529+
const DELETED_DYN_SLUG = "demo-deleted-node";
1530+
1531+
function deletedDynScript() {
1532+
return `import type { Document } from "@ai-di/graph-workflow/kinds";
1533+
1534+
/**
1535+
* @workflow-node
1536+
* @name ${DELETED_DYN_SLUG}
1537+
* @description Exists only to be deleted, so a workflow can reference a tombstone.
1538+
* @deterministic true
1539+
* @inputs { document: { kind: "Document", required: true } }
1540+
* @outputs { result: { kind: "Artifact" } }
1541+
*/
1542+
export default async function dynamicNode(
1543+
ctx: { document: Document },
1544+
_params: Record<string, unknown>,
1545+
): Promise<{ result: unknown }> {
1546+
return { result: ctx.document };
1547+
}`;
1548+
}
1549+
1550+
function deletedDynConfig(name) {
1551+
return {
1552+
schemaVersion: "1.0",
1553+
metadata: { name },
1554+
entryNodeId: "prep",
1555+
ctx: {
1556+
blobKey: { type: "string" },
1557+
documentId: { type: "string" },
1558+
preparedFileData: { type: "object" },
1559+
},
1560+
nodes: {
1561+
prep: {
1562+
id: "prep",
1563+
type: "activity",
1564+
label: "Prepare",
1565+
activityType: "file.prepare",
1566+
inputs: [
1567+
{ port: "documentId", ctxKey: "documentId" },
1568+
{ port: "blobKey", ctxKey: "blobKey" },
1569+
],
1570+
outputs: [{ port: "preparedData", ctxKey: "preparedFileData" }],
1571+
...pos(120, 140),
1572+
},
1573+
goneNode: {
1574+
id: "goneNode",
1575+
type: "activity",
1576+
label: "Custom step (lineage deleted)",
1577+
activityType: `dyn.${DELETED_DYN_SLUG}`,
1578+
inputs: [{ port: "document", ctxKey: "preparedFileData" }],
1579+
...pos(460, 140),
1580+
},
1581+
},
1582+
edges: [{ id: "e1", source: "prep", target: "goneNode", type: "normal" }],
1583+
};
1584+
}
1585+
1586+
const DELETED_DYN_STEPS = [
1587+
"This workflow references a custom node whose **lineage has been deleted**. Nothing to set up — it ships in that state.",
1588+
"The canvas node carries a red **Deleted** badge and its settings show *(deleted dynamic node)*.",
1589+
"The validation chip reads **1 error** — *Activity type `dyn.demo-deleted-node` is not registered*. Give the activity catalog a moment to load: `dyn.*` types are deliberately given the benefit of the doubt while it is still fetching, so the error appears a beat after the canvas.",
1590+
"⚠️ **Known gap (D-11):** **Try** and **Run** are still enabled here even though the graph cannot run — a run fails at `dynamicNode.resolveLineage`. Whether *any* validation error should disable Try, or only errors that make the graph structurally unrunnable, is an open product decision.",
1591+
"Restoring it is one step: publish a node with the **same name** from **Dynamic nodes ▸ New custom node** and the lineage comes back with its history continued (14.14).",
1592+
];
1593+
15131594
const DYN_DEMO_STEPS = [
15141595
"The **CUSTOM** section of the activity palette lists **demo-uppercase** with a **DYN** badge — click it to drop another instance.",
15151596
"The canvas node carries a purple **DYN** pill; select it → the Inputs section shows its `document` port bound to *Prepare*'s output.",
15161597
"Right-click the node → **Edit script** opens the script editor with the published TypeScript source (JSDoc `@inputs`/`@outputs` drive the ports).",
15171598
"**+ New custom node** (palette) opens the authoring editor — publishing runs the jsdoc → signature → ts-check → allowlist gates (`MANUAL_TEST_PLAN.md` Part 14).",
15181599
"**Delete + re-create restores the node:** delete this custom node (**Dynamic nodes** page), then **+ New custom node** and publish the *same* name — it comes back with its history continued (v2), instead of dead-ending on a reserved-slug conflict (14.14).",
1600+
"**Try it (14.9):** the script is tagged `@deterministic true`, so its output is **cached** and the node shows a real **preview** after a run — an untagged (non-deterministic) script re-executes every run and is deliberately never cached, so it has no preview to show.",
15191601
"⚠️ *Executing* this node in a run additionally needs the Temporal worker started with `PLATFORM_API_KEY` (14.9).",
15201602
];
15211603

@@ -1582,6 +1664,48 @@ async function createDemo(demo) {
15821664
* Returns its guide-result row, or null when the dynamic node couldn't be
15831665
* published.
15841666
*/
1667+
/**
1668+
* D2 — publish a lineage, build a workflow on it, then soft-delete the
1669+
* lineage so the demo ships already broken. Best-effort like its sibling: no
1670+
* deno-runner means no publish, so the demo is skipped rather than failing the
1671+
* whole seeder.
1672+
*/
1673+
async function createDeletedDynDemo() {
1674+
let published;
1675+
try {
1676+
published = await api("POST", "/api/dynamic-nodes", {
1677+
script: deletedDynScript(),
1678+
});
1679+
} catch {
1680+
console.log(" – deleted-dyn skipped (deno-runner unavailable)");
1681+
return null;
1682+
}
1683+
if (!published?.slug) return null;
1684+
const title = "Deleted custom node — Deleted badge & catalog error (Part 14)";
1685+
const name = `${NAME_PREFIX}${title}`;
1686+
const created = unwrap(
1687+
await api("POST", "/api/workflows", {
1688+
name,
1689+
config: withArrangeOnLoad(deletedDynConfig(name)),
1690+
groupId: GROUP_ID,
1691+
}),
1692+
);
1693+
// Tombstone the lineage AFTER the workflow references it — that ordering is
1694+
// the whole point of the fixture.
1695+
await api("DELETE", `/api/dynamic-nodes/${DELETED_DYN_SLUG}`);
1696+
console.log(
1697+
` ✓ ${"deleted-dyn".padEnd(14)} ${created.id} (dyn.${DELETED_DYN_SLUG} tombstoned)`,
1698+
);
1699+
return {
1700+
key: "deleted-dyn",
1701+
title,
1702+
steps: DELETED_DYN_STEPS,
1703+
id: created.id,
1704+
slug: created.slug,
1705+
dyn: true,
1706+
};
1707+
}
1708+
15851709
async function createDynamicNodeDemo() {
15861710
const dynSlug = await publishDemoDynamicNode();
15871711
if (!dynSlug) return null;
@@ -1624,14 +1748,15 @@ async function seed() {
16241748
// the previous one — reliable in practice because every create is a
16251749
// separate awaited HTTP round-trip.
16261750
const dynResult = await createDynamicNodeDemo();
1751+
const deletedDynResult = await createDeletedDynDemo();
16271752

16281753
const mainReversed = [];
16291754
for (let i = DEMOS.length - 1; i >= 0; i--) {
16301755
mainReversed.push(await createDemo(DEMOS[i]));
16311756
}
16321757
const mainResults = mainReversed.reverse();
16331758

1634-
return dynResult ? [...mainResults, dynResult] : mainResults;
1759+
return [...mainResults, dynResult, deletedDynResult].filter(Boolean);
16351760
}
16361761

16371762
function renderGuide(results, agentResults = []) {

0 commit comments

Comments
 (0)