Documentation for generating AppNode in the system, including rules, SDK usage, available data, and APIs. Must be loaded before generating or updating any AppNode.
Install
npx skillscat add antaymard/nolenor/app-node-generation-documentation Install via the SkillsCat registry.
AppNode Generation documentation
When generating or updating an "app" node, you MUST follow these strict rules.
App Node Generation Rules
You generate a single React component for an AppNode.
Strict rules
- The component MUST be named "App"
- No "import" statements — all libs are globals
- No "export default"
- React is global: use "React.useState", "React.useEffect", etc.
- Tailwind CSS classes are available
- Chart.js is available as global "Chart"
Mandatory workflow — ALWAYS follow this order
- Read source nodes first. Before writing any code, call "readNode" on each node connected to the AppNode. This gives you the exact schema: column names, data types, field names, and sample data. Never guess or assume data shapes.
- Design the UI based on the actual schema you just read. Reference the real column names and field names from step 1.
- Generate the component following the mandatory pattern below.
Available data & APIs
initialState (injected const, sync)
Previously saved state, or null on first run. Available immediately at first render — no async needed. You define the format; each app has its own state shape.
nolenor.getData() (async)
Returns all data from nodes connected via edges. No arguments needed.
async getData(): Promise<Record<string, {
id: string;
type: "table" | "blocknote" | "value" | "image" | "link" | "title" | "pdf";
name: string;
// --- table ---
columns?: { id: string; name: string; type: string }[];
// rows properties are flattened: row.annee NOT row.cells.annee
rows?: Record<string, any>[];
// --- blocknote ---
markdown?: string;
// --- value ---
value?: string | number;
label?: string;
unit?: string;
// --- image ---
url?: string; // first image URL
images?: { url: string }[]; // all images
// --- link ---
title?: string;
// --- title node ---
text?: string;
level?: string;
// --- pdf ---
files?: { url: string; filename: string; mimeType?: string }[];
}>>Important: For table rows, properties are directly on the row object. Use row.annee, not row.cells?.annee.
nolenor.saveState(state) (async)
Persists free-form JSON. Available as initialState on next mount.
async saveState(state: any): Promise<{ ok: true }>nolenor.fetch(url, options?) (async)
Makes an HTTP request proxied through the parent window. Use this to call public external APIs.
async fetch(url: string, options?: RequestInit): Promise<{
ok: boolean;
status: number;
statusText: string;
data: any; // parsed JSON if response is JSON, raw string otherwise
}>Security constraints — the following are automatically blocked and return { ok: false, status: 0 }:
localhostand loopback addresses (127.x.x.x,0.x.x.x)- Private IP ranges (
10.x.x.x,172.16–31.x.x,192.168.x.x,169.254.x.x) - Non-HTTP(S) protocols
No API key injection is available. All requests are made from the browser with no server-side secret. Only use public, unauthenticated endpoints, or endpoints that accept a key passed directly in the URL or headers (the user must hardcode it in the component code themselves).
Example — fetching a public JSON API:
React.useEffect(() => {
nolenor.fetch("https://api.example.com/data").then((res) => {
if (res.ok) setData(res.data);
else setError(res.statusText);
setLoading(false);
});
}, []);Example — with headers (e.g. a user-supplied API key):
nolenor.fetch("https://api.example.com/endpoint", {
method: "GET",
headers: {
Authorization: "Bearer <API_KEY_HERE>",
},
});Mandatory pattern
nolenor.getData() and nolenor.fetch() are async. You MUST:
- Initialize UI state from
initialStatesynchronously (filters, selections, toggles). - Fetch source data with
nolenor.getData()(and/ornolenor.fetch()) in auseEffect. - Show a loading indicator until data arrives.
- Never render the main UI before data is loaded.
const App = () => {
const [data, setData] = React.useState(null);
const [loading, setLoading] = React.useState(true);
// State restored from previous session (sync, available immediately)
const [filters, setFilters] = React.useState(initialState?.filters ?? []);
// Fetch source node data (async)
React.useEffect(() => {
nolenor.getData().then(d => {
setData(d);
setLoading(false);
});
}, []);
if (loading) {
return (
<div className="flex items-center justify-center h-full text-gray-400">
Chargement...
</div>
);
}
// data is guaranteed non-null below this point
const handleFilterChange = (f) => {
setFilters(f);
nolenor.saveState({ filters: f });
};
return ( /* your UI using data and filters */ );
};Refresh
Do NOT add a refresh button in the component code. The AppNode already has a built-in refresh button in its toolbar that reloads the iframe. There is no need to replicate this inside the component.
Runtime errors — debug loop
The iframe automatically reports runtime errors back to the canvas. You do NOT write any error-capture code. The bridge installs window.onerror, unhandledrejection, a console.error patch, and a React ErrorBoundary.
Errors are stored on the AppNode under values.errors and exposed when you read_nodes it. Each error has the shape:
{
type: "error" | "unhandledrejection" | "console.error" | "react" | "mount";
message: string; // truncated to 2,000 chars
stack?: string; // truncated to 4,000 chars
source?: string; // filename (uncaught errors only)
line?: number;
col?: number;
timestamp: number; // Date.now()
}- Errors are deduplicated by
(type|message|stack), debounced ~500ms, and capped at the 10 most recent. - When the AppNode's
codeis mutated (viaset_node_dataorpatch_app_node_code),values.errorsis automatically reset to[]and stale reports from the previous iframe version are dropped.
Mandatory debug loop after writing or patching code:
- Write or patch the code.
- Wait briefly, then
read_nodeson the AppNode. - Inspect
values.errors. If non-empty, diagnose fromtype+message+stack+line/coland patch again. - Repeat until
values.errorsstays empty across reads.
Don't declare the task done while errors are present.
Editing existing code — patch_app_node_code
For targeted edits to an existing AppNode (a few lines, a bug fix, a small refactor), use patch_app_node_code instead of rewriting the whole file with set_node_data. It's far more token-efficient and reduces the risk of regressions.
Use set_node_data with { code } only for the initial generation or a full rewrite.
Workflow
read_nodeson the AppNode to get the currentvalues.codeexactly (whitespace matters).- Build a single-block patch with one or more
@@hunks. - Call
patch_app_node_codewith{ nodeId, patch, explanation }(3-5 words forexplanation). - Run the debug loop above.
Patch format
*** Begin Patch
@@
unchanged context line (prefixed with a single space)
-line to remove (must match exactly)
+line to add
more context
@@
context for the next hunk
+pure insertion (no `-` line needed)
*** End PatchStrict rules
- One single
*** Begin Patch/*** End Patchblock per call. All hunks live inside it, separated by@@lines. - Every line inside a hunk MUST start with one of:
' '(context),'+'(added),'-'(removed). No bare blank lines — an empty context line must be" "(a single space). - Each hunk must contain at least one context line and at least one
+or-change. - The context + removed lines of a hunk must match the current code exactly once (whitespace and indentation included). If you get "no match", re-read the node — an earlier hunk in the same call may already have changed the region. If you get "multiple matches", add more context lines to disambiguate.
- Hunks are applied sequentially on the cumulative source. All-or-nothing: if any hunk fails (no match, multiple matches, malformed), no change is written and the tool returns a structured error pointing to the offending hunk.
When patching fails
- Re-
read_nodesto get fresh code (a previous patch may have shifted things). - Increase the context window around your change.
- If the change spans many disjoint regions or rewrites a large block, fall back to
set_node_datawith the full{ code }.