@rolealpha/n8n-nodes-rolealpha 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,8 +45,8 @@ next run follows. You don't edit code and you don't republish anything.
45
45
  - **Teams of agents on one step.** Several roles share a step. Each agent does its own part,
46
46
  waits for what it needs and is woken when it arrives. Agents that hang are escalated to
47
47
  the role that leads their circle.
48
- - **Workflows that follow the organisation.** When a step, role or agent is created, changed
49
- or removed in roleALPHA, a model proposes what that means for the n8n workflows. The
48
+ - **Workflows that follow the organisation.** When a step is created or changed in roleALPHA,
49
+ or an entity a workflow is bound to changes or is removed, a model proposes what that means for the n8n workflows. The
50
50
  proposal becomes an inactive draft, and only a person filling the deciding role can apply it.
51
51
  - **Limits you can change without republishing.** Retries, wait times, run time and tool
52
52
  calls come from a settings table and are clamped to fixed bounds.
@@ -137,10 +137,9 @@ Builder** closes that gap without letting a model change production on its own.
137
137
 
138
138
  ```
139
139
  Scheduler (every 15 min)
140
- └ Data table: snapshot rows
141
- └ Workflow Builder: Detect Changes new / changed / removed steps, roles, agents
142
- ├ store and remove snapshot rows
143
- └ per change: Workflow Builder: Propose ← chat model
140
+ └ Workflow Builder: Detect Changes list_changes since the last run: new / changed
141
+ │ steps, changed / removed bound entities
142
+ └ per change: Workflow Builder: Propose ← chat model
144
143
  └ Create Draft inactive, tagged rolealpha-draft
145
144
  └ roleALPHA: Get Decider (governing step)
146
145
  └ per person: Issue Approval Tokens → e-mail with approve / reject link
@@ -152,13 +151,31 @@ Confirm button (POST)
152
151
  └ reject → Workflow Builder: Discard Draft
153
152
  ```
154
153
 
155
- **Detect** compares a fingerprint of every watched entity (name, custom ID, attributes and
156
- edges) with the rows stored last time. The first run only stores the snapshot. An entity that
157
- cannot be read is neither changed nor removed; removals count only when the complete index was
158
- read.
154
+ **Detect** asks roleALPHA's `list_changes` what changed effectively since the last run:
155
+ approved drafts and every relationship change, fanned out to each end. That is one call per
156
+ run, whatever the size of the organisation. It keeps the cursor in the workflow's static data,
157
+ so no table is needed; the first run starts now and reports nothing from before. What counts:
158
+
159
+ - new and changed entities of the watched types (default: steps), and
160
+ - changed entities a managed workflow is bound to, whatever their type.
161
+
162
+ What does not: relationships the runner reads at run time (who decides, is accountable,
163
+ responsible, consulted or informed, who holds a role, which policy applies). A new holder or a new decider changes no workflow, so it costs no model
164
+ call. A change names what changed, not the old values: the model compares the entity as it is
165
+ now with what the workflows do. **Skip Own Changes** also leaves out what the connection's
166
+ account proposed or approved (`ownChange`); use it only with an account of its own, since with
167
+ a personal login it would leave out that person's approvals too.
168
+
169
+ A deleted entity does not appear in the feed itself; its neighbours get a relationship whose
170
+ other end is gone. Then, and when the cursor was lost, Detect asks for every bound entity
171
+ whether it still exists. Only "not found" counts as removed; an entity it cannot tell is asked
172
+ for again next run. A failed read of the feed fails the run and keeps the old cursor.
159
173
 
160
174
  **Propose** gives the model the change, the entity as roleALPHA shows it now (a step with its
161
- roles, never persons), the managed workflows and the credentials they use. Its answer is
175
+ roles, never persons), the managed workflows, which of them the change affects (bound to the
176
+ entity, or to a step right before, after, above or below it) and the credentials they use. An
177
+ update for a neighbouring step leaves the binding as it is: the workflow does not become bound
178
+ to the neighbour. Its answer is
162
179
  checked, and a failed check goes back to the model once more with the problems:
163
180
 
164
181
  - only node types from the allow list (no Code node by default),
@@ -183,29 +200,16 @@ Opening a link only shows a confirm page; mail scanners that open links decide n
183
200
  ### Setting it up
184
201
 
185
202
  Two ready workflows are in [`examples/`](examples). Installing the package does not set them
186
- up: n8n runs no code of a community package at install, so the data table and the credentials
187
- have to be created once by hand.
203
+ up: n8n runs no code of a community package at install, so the credentials have to be created
204
+ once by hand. No data table is needed; *Detect Changes* keeps its cursor in the workflow.
188
205
 
189
- 1. **Create the data table.** In n8n open the project the workflows will live in (e.g.
190
- *Personal*), go to the tab *Data tables*, create a table named `rolealpha_snapshot` and add
191
- these columns, all of type **String**:
192
-
193
- | Column | Holds |
194
- |---|---|
195
- | `uuid` | UUID of the entity (key of the row) |
196
- | `entity_type` | `process`, `role`, `agent` … |
197
- | `name` | Name at the last run |
198
- | `custom_id` | Custom ID, may be empty |
199
- | `fingerprint` | Hash over name, custom ID, attributes and edges |
200
- | `fields` | JSON: a short hash per attribute |
201
- | `edges` | JSON: the edges as `TYPE>uuid` / `TYPE<uuid` |
202
- | `seen_at` | Time of the run that stored the row |
203
-
204
- The name must be exactly `rolealpha_snapshot`; the example finds the table by name. Without
205
- it, *Read snapshot* fails and nothing is detected.
206
+ 1. **Release the modules for AI in roleALPHA** (*AI settings → data release*). Without a
207
+ released module `list_changes` fails instead of reporting nothing.
206
208
  2. **Create the credentials** (see [Credentials](#credentials)):
207
- - **MCP OAuth2** for roleALPHA. It must read the whole organisation, not just one agent.
208
- - **n8n API** with an API key from *Settings → n8n API*. When `/api` is not reachable from
209
+ - **MCP OAuth2** for roleALPHA. It sees what its access may see: steps hidden from it are
210
+ neither watched nor reported.
211
+ - **n8n API** with an API key from *Settings → n8n API*. *Detect Changes* uses it too, to
212
+ read which entities the managed workflows are bound to. When `/api` is not reachable from
209
213
  outside, use `http://localhost:5678/api/v1` as base URL.
210
214
  - **Approval Seal API** with a random key of at least 32 characters. It signs the approval
211
215
  links; keep it secret, anybody with it can make links.
@@ -218,8 +222,9 @@ have to be created once by hand.
218
222
  5. **Set up the governing step in roleALPHA**, as described in the next section. Without it
219
223
  nobody may approve, and every draft stays a draft.
220
224
  6. **Activate** *Approve workflow drafts* first, then *Workflows follow the organisation*. The
221
- first run only fills the data table and reports nothing; changes are reported from the
222
- second run on.
225
+ first run starts now and only checks that the bound entities still exist; changes are
226
+ reported from the second run on. The cursor is kept only by runs of the active workflow:
227
+ a manual test run starts afresh each time.
223
228
 
224
229
  ### The governing step in roleALPHA
225
230
 
@@ -286,7 +291,7 @@ one mail per role.
286
291
 
287
292
  | Operation | What it does | Credential |
288
293
  |---|---|---|
289
- | Detect Changes | Compares the watched entity types (default: process, role, agent) with the stored snapshot rows. Returns `changes` (`created`, `changed` with `changedFields`, `edgesAdded`, `edgesRemoved`, `removed`), the rows to `upsert` and `remove`, and `unreadable`. | MCP OAuth2 |
294
+ | Detect Changes | Reads `list_changes` from the cursor kept in the workflow (or the **Cursor** option). Returns `changes` (`created`, `changed`, `removed`, each with `relationships` and the bound `workflows`), `cursor`, `hasMore`, `started`, `checked`, `unreadable` and `skipped`. | MCP OAuth2, n8n API |
290
295
  | List Workflows | The managed workflows with their bound entities and the credentials they use. | n8n API |
291
296
  | Propose | Needs a chat model on its **Model** input. Returns `ok`, `action` (`create`, `update`, `deactivate`, `none`), `reason`, `workflow`, `contentHash`, `problems` and `notes`. | MCP OAuth2, n8n API |
292
297
  | Create Draft | Stores a proposal with `ok = true` as an inactive draft. Returns `draftId` and `contentHash`. | n8n API |
@@ -458,13 +463,15 @@ not caught by the graph.
458
463
  | `roleALPHA refused the run: agent_nicht_gebunden` | The function account may not run this agent. Add the agent under "May run these agents" of the account. |
459
464
  | `roleALPHA refused the run: kein_agent_run_recht` | No role of the function account has `agent:run`. |
460
465
  | `Acting as an agent needs a function account` | The MCP OAuth2 credential uses another grant type. Use the client ID and secret of a function account. |
461
- | `MCP initialize failed with HTTP 429` | Many runs started at once and roleALPHA turned some away. The client repeats a call turned away with 429 or 503 up to four times (Retry-After, else 1, 2, 4, 8 s). If it still fails, start fewer runs at once. |
466
+ | `MCP initialize failed with HTTP 429` | Many runs started at once and roleALPHA turned some away (120 calls per minute and access). The client repeats a call turned away with 429 or 503 up to four times and waits as long as the server asks (Retry-After or RateLimit-Reset, else 1, 2, 4, 8 s). If it still fails, start fewer runs at once. |
462
467
  | `Empty MCP response` | The server answered with no body. Reading calls are repeated; a writing tool is not, so it cannot run twice. |
463
468
  | `Cannot access "arguments" due to security concerns` | n8n blocks the name `arguments` in expressions and in `$fromAI`. Name the tool input `toolArgs` or `args`. |
464
469
  | Propose returns `ok: false` | The model's answer failed the check twice; `problems` says why. Often a node type outside the allow list or a hard-coded address. Add what the model needs to **Extra Rules**. |
465
470
  | `The draft changed after it was proposed or approved` | Somebody edited the draft, or the link carries another hash. Discard it; the next change proposes again. |
466
471
  | `Workflow … changed after the draft was made` | The target was edited after the draft. Discard the draft and let the builder propose again. |
467
- | Detect fails with `more than Max Entities` | Every watched entity costs two reads per run. Watch fewer types or raise the limit. |
472
+ | Detect fails with `kein Modul für die KI freigegeben` | No module is released for AI in roleALPHA. Release it under *AI settings → data release*. |
473
+ | Detect reports `hasMore: true` | More changes waited than **Max Pages** × 500. The next run goes on from there. |
474
+ | Detect reports the same change twice across runs | `list_changes` delivers at least once, after a restore of the tenant also known changes again. Proposals for them usually come back `none`. |
468
475
  | Webhook answers `401` | The `X-RA-Signature` does not match. Check the secret in **roleALPHA Webhook API**. |
469
476
 
470
477
  ## Licence
@@ -1,92 +1,147 @@
1
1
  /**
2
- * Change detection in the roleALPHA graph. roleALPHA sends no event when a step, role or
3
- * agent is created or changed, so a scheduler takes a snapshot and compares it with the
4
- * rows it stored the last time.
2
+ * Change detection in the roleALPHA graph. roleALPHA's list_changes names the entities that
3
+ * changed effectively since a cursor: approved drafts and every relationship change, fanned out
4
+ * to each end. One call per run replaces reading the whole organisation, so the cost grows with
5
+ * the changes, not with the size of the organisation.
5
6
  *
6
- * Fail-closed: an entity that could not be read is neither new nor changed nor removed;
7
- * its old row stays. Removals are reported only when the complete index was read.
7
+ * What counts: entities a managed workflow is bound to, and entities of the watched types
8
+ * (a new or changed step may need a workflow). What does not: relationships the runner resolves
9
+ * at run time, and on request the access's own writes (who decides, who holds a role, which policy
10
+ * applies). Those never require a workflow to change.
11
+ *
12
+ * A deleted entity does not appear itself; its neighbours get a relationship row whose other
13
+ * end is null. Such a row, a lost cursor or an earlier unreadable check makes the detector ask
14
+ * for every bound entity whether it still exists. Fail-closed: only "not found" counts as removed.
8
15
  */
9
- import type { GraphSource, Relationship } from './graph';
10
16
  import type { McpClient } from './mcp';
11
- /** Entity types of the lean index (list_entities). */
17
+ /** Entity types list_changes can filter on and Detect Changes can watch. */
12
18
  export declare const ENTITY_TYPES: readonly ["process", "role", "agent", "policy", "organigramm", "project", "okr", "person"];
13
- export interface IndexEntry {
14
- uuid: string;
15
- name: string;
16
- entityType: string;
17
- }
18
- export interface EntitySnapshot {
19
- uuid: string;
20
- name: string;
21
- /** Type of the lean index (process, role, agent, …). */
19
+ /** Relationships the runner reads at run time: a change to them never changes a workflow. */
20
+ export declare const RUNTIME_RELATIONSHIPS: string[];
21
+ /** Relationships that make a step a neighbour of another: the order and the breakdown of steps. */
22
+ export declare const NEIGHBOUR_RELATIONSHIPS: string[];
23
+ /** One row of list_changes. */
24
+ export interface FeedRow {
25
+ entityUuid: string;
22
26
  entityType: string;
23
- customId: string | null;
24
- /** Short hash per attribute, so a change names the attributes that changed. */
25
- fields: Record<string, string>;
26
- /** `TYPE>uuid` (outgoing) and `TYPE<uuid` (incoming), sorted. */
27
- edges: string[];
28
- fingerprint: string;
27
+ changedAt: string;
28
+ change: 'created' | 'updated' | 'deleted' | 'relationship';
29
+ origin?: string;
30
+ ownChange?: boolean;
31
+ via?: {
32
+ relationshipId?: number | string;
33
+ relTypeId?: number;
34
+ relationshipType: string;
35
+ otherEntityUuid: string | null;
36
+ role?: 'from' | 'to' | 'participant';
37
+ action: 'added' | 'changed' | 'removed';
38
+ };
29
39
  }
30
- /** A stored snapshot row. `fields` and `edges` are JSON strings so any table can hold them. */
31
- export interface SnapshotRow {
32
- uuid: string;
33
- entity_type: string;
34
- name: string;
35
- custom_id: string | null;
36
- fingerprint: string;
37
- fields: string;
38
- edges: string;
39
- seen_at: string;
40
+ export interface RelationshipChange {
41
+ type: string;
42
+ action: 'added' | 'changed' | 'removed';
43
+ /** The other end; null when it was deleted. */
44
+ otherUuid: string | null;
45
+ /** Which end this entity is: from, to or participant. */
46
+ end: string | null;
40
47
  }
41
48
  export type ChangeKind = 'created' | 'changed' | 'removed';
49
+ /**
50
+ * One entity that changed. It names what changed, not the old values: the model compares the
51
+ * entity as it is now with what the workflows do. Name and custom ID are filled in by Propose.
52
+ */
42
53
  export interface EntityChange {
43
54
  change: ChangeKind;
44
55
  uuid: string;
45
- name: string;
46
56
  entityType: string;
57
+ name: string;
47
58
  customId: string | null;
48
- previousName: string | null;
49
- changedFields: string[];
50
- edgesAdded: string[];
51
- edgesRemoved: string[];
59
+ changedAt: string | null;
60
+ relationships: RelationshipChange[];
61
+ /** IDs of the managed workflows bound to this entity. */
62
+ workflows: string[];
52
63
  }
53
- export interface DetectResult {
54
- /** No stored rows yet: the snapshot is stored and nothing is reported. */
55
- baseline: boolean;
56
- /** Whether the complete index was read; only then are removals reported. */
57
- complete: boolean;
64
+ export interface FeedSummary {
58
65
  changes: EntityChange[];
59
- /** Rows to insert or replace (by uuid). */
60
- upsert: SnapshotRow[];
61
- /** UUIDs whose rows are deleted. */
62
- remove: string[];
63
- /** Entities that could not be read this time; their rows stay as they are. */
66
+ /** A relationship lost its other end: something was deleted, maybe a bound entity. */
67
+ deletionSeen: boolean;
68
+ skipped: {
69
+ own: number;
70
+ runtime: number;
71
+ unwatched: number;
72
+ };
73
+ }
74
+ /** Rows as list_changes returns them; drops rows without entity or change. */
75
+ export declare function normalizeFeedRows(raw: unknown): FeedRow[];
76
+ /**
77
+ * Turns feed rows into one change per entity. `bound` maps entity UUIDs to the managed
78
+ * workflows bound to them; `watchTypes` are the types whose new and changed entities count
79
+ * even without a workflow.
80
+ */
81
+ export declare function summarizeFeed(rows: FeedRow[], opts: {
82
+ bound: Map<string, string[]>;
83
+ watchTypes: string[];
84
+ ignoreRelationships?: string[];
85
+ skipOwn?: boolean;
86
+ }): FeedSummary;
87
+ export interface FeedRead {
88
+ rows: FeedRow[];
89
+ cursor: string;
90
+ /** More rows wait; the next run goes on from `cursor`. */
91
+ hasMore: boolean;
92
+ pages: number;
93
+ }
94
+ /**
95
+ * Reads list_changes from a cursor, or on the first run from `since`, page by page.
96
+ * A failed call throws, so the stored cursor stays where it was.
97
+ */
98
+ export declare function readFeed(client: McpClient, organisation: string, start: {
99
+ cursor?: string;
100
+ since?: string;
101
+ }, opts?: {
102
+ maxPages?: number;
103
+ limit?: number;
104
+ }): Promise<FeedRead>;
105
+ export interface ExistenceCheck {
106
+ removed: string[];
107
+ /** Entities whose existence could not be told; checked again next run. */
64
108
  unreadable: Array<{
65
109
  uuid: string;
66
- name: string;
67
110
  error: string;
68
111
  }>;
69
112
  }
70
- export declare function snapshotOf(entry: IndexEntry, details: {
71
- name: string;
72
- customId: string | null;
73
- data: Record<string, unknown>;
74
- }, rels: Relationship[]): EntitySnapshot;
75
- export declare function toRow(s: EntitySnapshot, seenAt: string): SnapshotRow;
76
- /** Rows as they come back from a table: drops rows without uuid or fingerprint. */
77
- export declare function normalizeRows(raw: unknown): SnapshotRow[];
78
- export declare function detectChanges(previousRaw: unknown, current: EntitySnapshot[], opts: {
79
- complete: boolean;
80
- types: string[];
81
- unreadable?: DetectResult['unreadable'];
82
- now?: Date;
83
- reportBaseline?: boolean;
84
- }): DetectResult;
85
- /** Reads the complete lean index of one type, page by page. */
86
- export declare function listIndex(client: McpClient, organisation: string, entityType: string, maxPages?: number): Promise<IndexEntry[]>;
87
- export interface SnapshotRead {
88
- snapshots: EntitySnapshot[];
89
- unreadable: DetectResult['unreadable'];
113
+ /** Asks for every UUID whether it still exists. Only a plain "not found" counts as removed. */
114
+ export declare function checkExistence(client: McpClient, organisation: string, uuids: string[]): Promise<ExistenceCheck>;
115
+ /** What Detect Changes keeps between runs, in the workflow's static data. */
116
+ export interface FeedState {
117
+ cursor?: string;
118
+ /** An existence check could not finish; run it again. */
119
+ recheck?: boolean;
90
120
  }
91
- /** Reads details and relationships of every index entry. A failed read marks the entity unreadable. */
92
- export declare function readSnapshots(src: GraphSource, entries: IndexEntry[], concurrency?: number): Promise<SnapshotRead>;
121
+ export interface DetectResult {
122
+ changes: EntityChange[];
123
+ cursor: string;
124
+ hasMore: boolean;
125
+ /** No cursor was stored: the run started at `since` and checked the bound entities. */
126
+ started: boolean;
127
+ /** Bound entities whose existence was checked. */
128
+ checked: number;
129
+ unreadable: ExistenceCheck['unreadable'];
130
+ skipped: FeedSummary['skipped'];
131
+ state: FeedState;
132
+ }
133
+ export declare function detectChanges(client: McpClient, organisation: string, state: FeedState, opts: {
134
+ bound: Map<string, string[]>;
135
+ watchTypes: string[];
136
+ ignoreRelationships?: string[];
137
+ skipOwn?: boolean;
138
+ since?: string;
139
+ maxPages?: number;
140
+ now?: Date;
141
+ }): Promise<DetectResult>;
142
+ /** Bound entity UUIDs of managed workflows (drafts excluded) to the IDs of those workflows. */
143
+ export declare function boundEntities(workflows: Array<{
144
+ id: string;
145
+ entities: string[];
146
+ isDraft: boolean;
147
+ }>): Map<string, string[]>;