@pipefy/pipefy-process-coder 0.1.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/.agents/skills/ppc-pipefy-flow-authoring/SKILL.md +262 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-danfe-consulta.json +247 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-webhook-retorno-consulta.json +589 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-4-recebimento-barramento.json +1391 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-2-danfe-retorno.json +623 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-4-criacao-operacao.json +636 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/03-subflow-4-criacao-titulo.json +3642 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-cedente.json +863 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-sacado.json +799 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/README.md +42 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-acompanhamento-cobranca.json +581 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-nfe-monitoramento.json +503 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-retorno-bancario.json +562 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-cedente.json +557 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-sacado.json +609 -0
- package/.agents/skills/ppc-pipefy-pipe-authoring/SKILL.md +146 -0
- package/.agents/skills/ppc-pipefy-process-design/SKILL.md +127 -0
- package/.agents/skills/ppc-pipefy-workspace/SKILL.md +91 -0
- package/AGENTS.md +456 -0
- package/README.md +808 -0
- package/bin/pipe.js +13 -0
- package/package.json +35 -0
- package/src/apply/adopt.ts +150 -0
- package/src/apply/agentops.ts +71 -0
- package/src/apply/compile.ts +875 -0
- package/src/apply/execute.ts +336 -0
- package/src/apply/flowops.ts +399 -0
- package/src/apply/idmap.ts +241 -0
- package/src/apply/mutations.ts +955 -0
- package/src/apply/registry.ts +430 -0
- package/src/apply/types.ts +134 -0
- package/src/cli/args.ts +88 -0
- package/src/cli.ts +211 -0
- package/src/codec/flow.ts +199 -0
- package/src/codec/pack.ts +103 -0
- package/src/codec/roundtrip.ts +94 -0
- package/src/codec/unpack.ts +198 -0
- package/src/commands/agents.ts +184 -0
- package/src/commands/apply.ts +1144 -0
- package/src/commands/context.ts +119 -0
- package/src/commands/create.ts +79 -0
- package/src/commands/diff.ts +314 -0
- package/src/commands/flows.ts +414 -0
- package/src/commands/misc.ts +644 -0
- package/src/commands/plan.ts +331 -0
- package/src/commands/pull.ts +567 -0
- package/src/commands/runs.ts +83 -0
- package/src/commands/skills.ts +137 -0
- package/src/commands/verify.ts +253 -0
- package/src/config.ts +168 -0
- package/src/diff/agents.ts +122 -0
- package/src/diff/diff.ts +1130 -0
- package/src/diff/flow.ts +318 -0
- package/src/diff/html.ts +322 -0
- package/src/diff/render.ts +101 -0
- package/src/model/payload.ts +154 -0
- package/src/model/tree.ts +99 -0
- package/src/model/volatile.ts +55 -0
- package/src/pipefy/agents.ts +165 -0
- package/src/pipefy/automations.ts +219 -0
- package/src/pipefy/capability.ts +119 -0
- package/src/pipefy/client.ts +267 -0
- package/src/pipefy/discovery.ts +209 -0
- package/src/pipefy/internal.ts +380 -0
- package/src/pipefy/ipaas.ts +365 -0
- package/src/pipefy/reconstruct.ts +775 -0
- package/src/pipefy/reference.ts +251 -0
- package/src/pipefy/snapshot.ts +245 -0
- package/src/pipefy/toolkit.ts +200 -0
- package/src/pipefy/toolkit_bearer.py +137 -0
- package/src/report/integrations.ts +231 -0
- package/src/report/run.ts +475 -0
- package/src/util/fsx.ts +45 -0
- package/src/util/git.ts +32 -0
- package/src/util/json.ts +55 -0
- package/src/util/log.ts +76 -0
- package/src/util/pool.ts +48 -0
- package/src/util/slug.ts +26 -0
- package/src/util/tui.ts +335 -0
- package/src/validate/index.ts +123 -0
- package/src/validate/integrity.ts +387 -0
- package/src/validate/reference.ts +136 -0
- package/src/validate/schema.ts +328 -0
- package/src/workspace/agents.ts +290 -0
- package/src/workspace/docs.ts +407 -0
- package/src/workspace/flows.ts +191 -0
- package/src/workspace/layout.ts +165 -0
- package/src/workspace/lock.ts +148 -0
- package/src/workspace/read.ts +165 -0
- package/src/workspace/reference.ts +24 -0
- package/src/workspace/stamp.ts +301 -0
- package/src/workspace/write.ts +225 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
# AGENTS.md — working on pipefy-process-coder
|
|
2
|
+
|
|
3
|
+
Read this before changing anything. It is the accumulated cost of getting this
|
|
4
|
+
tool wrong in specific ways, written down so the next agent does not pay it again.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. What this is, and what it is for
|
|
9
|
+
|
|
10
|
+
`pipe` is a CLI that pulls a Pipefy pipe — **and its iPaaS flows** — to disk as
|
|
11
|
+
readable JSON, lets you edit the files, diffs them, compiles the diff into API
|
|
12
|
+
calls, applies them, and verifies the result.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
pipe pull <pipeId> --out ./my-pipe # the pipe AND its iPaaS flows
|
|
16
|
+
# edit the JSON
|
|
17
|
+
pipe validate # shape + referential integrity, offline
|
|
18
|
+
pipe diff # what changed — pipe structure and flows
|
|
19
|
+
pipe plan --dry-run # is every change applicable? refuses if not
|
|
20
|
+
pipe apply # snapshot, apply step by step, verify, adopt
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**The app is the deliverable.** Whatever pipes appear in `workspaces/` are
|
|
24
|
+
*fixtures* — they exist to exercise the tool against a real endpoint, and they are
|
|
25
|
+
gitignored for a reason. Do not optimise for a good-looking pipe. A fixture change
|
|
26
|
+
that exercises an untested path is worth making; one that only makes the fixture
|
|
27
|
+
nicer is not.
|
|
28
|
+
|
|
29
|
+
The tool's job is to make a client's process **auditable and changeable as code**,
|
|
30
|
+
on a laptop, against an API that lies in specific documented ways. It is built for
|
|
31
|
+
an FDE working on someone else's Pipefy account, which is why every write is
|
|
32
|
+
planned, refusable and verified.
|
|
33
|
+
|
|
34
|
+
Specific ids appear in two places on purpose: `VALIDATION.md` and the run logs
|
|
35
|
+
under `workspaces/*/flows/BUILD-LOG.md` are **records of real runs** kept locally
|
|
36
|
+
and outside git, and an id
|
|
37
|
+
there is evidence. Everywhere else — source comments, help text, these docs, the
|
|
38
|
+
skills — ids are placeholders, because a general tool should not read as though it
|
|
39
|
+
were written for one customer.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 2. The one rule that matters most
|
|
44
|
+
|
|
45
|
+
**Build through the app. Validate with the MCP afterwards.**
|
|
46
|
+
|
|
47
|
+
When `pipe` cannot do something you need, the answer is to *add it to `pipe`* —
|
|
48
|
+
not to reach for the Pipefy MCP, not to write a script in `research/` that does
|
|
49
|
+
the write for you. Routing around a gap hides exactly the gap that was worth
|
|
50
|
+
finding: the work looks finished and the tool is no better than before.
|
|
51
|
+
|
|
52
|
+
- A blocker met with a workaround teaches nothing.
|
|
53
|
+
- A blocker met by extending the app is the whole job.
|
|
54
|
+
- A limitation of the **platform** is worth reporting. A limitation of the **app**
|
|
55
|
+
is worth fixing.
|
|
56
|
+
|
|
57
|
+
The Pipefy MCP is for *independent verification after the build* — a different
|
|
58
|
+
transport and a different code path, which makes it a genuine second opinion. Use
|
|
59
|
+
`ap_validate_flow`, `ap_flow_structure`, `get_pipe`, `execute_graphql` to check
|
|
60
|
+
what the app did. Never to do it.
|
|
61
|
+
|
|
62
|
+
Anything that writes to a client's pipe belongs in `src/`, behind a command, with
|
|
63
|
+
a plan step and a verify. `research/` holds reads and probes.
|
|
64
|
+
|
|
65
|
+
Three probes there *do* write, because that is how the operation model and the
|
|
66
|
+
connection shapes were learned. Each refuses to run without `PIPE_PROBE_WRITES=1`,
|
|
67
|
+
and the reason is in §4. Do not add a fourth to get past a gap in the app.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 3. The doctrines that make this trustworthy
|
|
72
|
+
|
|
73
|
+
These are not style. Each one exists because its absence caused a live incident.
|
|
74
|
+
|
|
75
|
+
### Coverage: absence is not absence
|
|
76
|
+
|
|
77
|
+
Every entity group carries `complete | partial | unknown`. An `unknown` group
|
|
78
|
+
means the read path could not see it — **not** that the pipe has none. Such a
|
|
79
|
+
group is never diffed and never applied.
|
|
80
|
+
|
|
81
|
+
This is the difference between "this pipe has no webhooks" and "I cannot read
|
|
82
|
+
webhooks", and conflating them licenses deleting a client's live integrations.
|
|
83
|
+
Applies to iPaaS flows identically: a pipe whose iPaaS is off gets `unknown`, and
|
|
84
|
+
its flows are not compared at all.
|
|
85
|
+
|
|
86
|
+
### Refuse rather than guess, per column
|
|
87
|
+
|
|
88
|
+
`src/apply/registry.ts` records, for every entity and operation, whether a write
|
|
89
|
+
path exists — and `settableColumns` records which *columns* it can actually
|
|
90
|
+
write. `pipe plan` refuses what has no path, naming the reason.
|
|
91
|
+
|
|
92
|
+
The failure this prevents: **a mutation that succeeds and changes nothing.**
|
|
93
|
+
Arguments are filtered against the live schema, so an argument the mutation has no
|
|
94
|
+
input for is silently dropped and the call returns success. `updatePipe` did this
|
|
95
|
+
with `description`, `anyone_can_create_card` and `create_card_label`. A workspace
|
|
96
|
+
that set one could never converge, and nothing said why.
|
|
97
|
+
|
|
98
|
+
### Round trip or it does not ship
|
|
99
|
+
|
|
100
|
+
`pack(unpack(P)) == P` for the payload codec, `link(unlink(F)) == F` for the flow
|
|
101
|
+
codec. The payload fold is *movement only*, which makes it true by construction;
|
|
102
|
+
the flow fold is a real transformation, so `test/flow-codec.test.ts` earns it
|
|
103
|
+
across a linear chain, a router with an empty branch, a loop with a body, a router
|
|
104
|
+
nested in a loop, and the live flows.
|
|
105
|
+
|
|
106
|
+
If you add a fold, prove it the same way or keep the raw JSON.
|
|
107
|
+
|
|
108
|
+
### Verify against intent, not identity
|
|
109
|
+
|
|
110
|
+
After an apply, the tool reads the pipe back and diffs `live → expected` with
|
|
111
|
+
`intentOnly`. An authored entity is a *partial specification*: the author writes
|
|
112
|
+
what they care about and the server fills the rest. Asking "are these rows
|
|
113
|
+
identical" fails a perfectly correct apply.
|
|
114
|
+
|
|
115
|
+
Never assert a column the server mints and no create path accepts — `uuid`,
|
|
116
|
+
`slug`, `color_uuid`, `suid`. That mistake reported 60 successful creates as
|
|
117
|
+
failures.
|
|
118
|
+
|
|
119
|
+
### Nothing leaves carrying a placeholder
|
|
120
|
+
|
|
121
|
+
A new entity has no id, so a reference to it is `%{_new:<uuid>}`, substituted at
|
|
122
|
+
send time from `IdMap`. `ID_BEARING_KEYS` governs which keys get a bare-value
|
|
123
|
+
substitution, and the executor **refuses to send any request still containing
|
|
124
|
+
`_new:`**, naming the key path.
|
|
125
|
+
|
|
126
|
+
That list has been the cause of three separate bugs — `fieldConditionIds`, then
|
|
127
|
+
`phase[jump_target_ids][]`. When you add a key that holds an id, add it there.
|
|
128
|
+
|
|
129
|
+
### The create response already has the id — a read-back is not the only way to learn it
|
|
130
|
+
|
|
131
|
+
`IdMap` is built live during `execute.ts`, straight from each create mutation's
|
|
132
|
+
own response, to resolve `%{_new:<uuid>}` for later steps in the same run. That
|
|
133
|
+
map is also enough to adopt a created entity's id into the workspace file it
|
|
134
|
+
came from (`apply/adopt.ts`) — no live read-back needed, and by construction
|
|
135
|
+
never wrong, because it is the id the mutation that created the entity actually
|
|
136
|
+
returned.
|
|
137
|
+
|
|
138
|
+
This is why `pipe apply` no longer snapshots a second time on its own
|
|
139
|
+
initiative: that second, live-read snapshot (`SnapshotClient.pull()` always
|
|
140
|
+
creates one — snapshot.ts's own comment says so) was the thing making the
|
|
141
|
+
pre-apply rollback snapshot stop being restorable the moment the apply that
|
|
142
|
+
took it finished. Id adoption never needed the read; only a *fuller*
|
|
143
|
+
comparison (server-minted `slug`, `color_uuid`, and the like) does, and that
|
|
144
|
+
stays opt-in (`--post-snapshot`, or the prompt).
|
|
145
|
+
|
|
146
|
+
Every `produces` entry on a create step now matters for this reason too, not
|
|
147
|
+
only for cross-step reference resolution — a create with no `produces` (labels,
|
|
148
|
+
webhooks and pipe_relations lacked one until this was noticed live: `pipe
|
|
149
|
+
apply` reported the id adopted, and the file still read `"id": null, "_new":
|
|
150
|
+
true"`) silently cannot be adopted this way.
|
|
151
|
+
|
|
152
|
+
### One apply, one revert question — not three mechanisms wearing one word
|
|
153
|
+
|
|
154
|
+
The first version of this had three unrelated things all called some kind of
|
|
155
|
+
"rollback": a pipe-structure offer wired to `pipe apply`'s pipe-side failure,
|
|
156
|
+
a flow-only offer wired to the flow half, and nothing at all for agents. That
|
|
157
|
+
is not how a user experiences an apply — it is one action, and if any part of
|
|
158
|
+
it fails partway, the honest question is "revert everything this apply
|
|
159
|
+
changed", asked once, with the details of what that actually means for each
|
|
160
|
+
part that touched anything. `offerUnifiedRevert` (apply.ts) is that one
|
|
161
|
+
question; `finishApply` is what makes sure every exit from `applyOneRepo`
|
|
162
|
+
that could still touch flows or agents runs through it.
|
|
163
|
+
|
|
164
|
+
It is also asked when the failure is *not* in the half that already
|
|
165
|
+
succeeded: a pipe change that lands cleanly and is then followed by a flow or
|
|
166
|
+
agent failure still gets offered as part of the same revert, because from the
|
|
167
|
+
user's side "this apply" covers all of it, not just whichever half happened
|
|
168
|
+
to error.
|
|
169
|
+
|
|
170
|
+
Each part is reverted the way its own platform actually allows, because
|
|
171
|
+
there is no single API that reverses all three:
|
|
172
|
+
|
|
173
|
+
- **Pipe structure** — `restoreRepoToSnapshot`, Pipefy's own mechanism,
|
|
174
|
+
restoring the pre-apply snapshot. Only offered while that snapshot is still
|
|
175
|
+
the latest (`caps.restoreLatestOnly`) — nothing before this point takes a
|
|
176
|
+
second snapshot on its own initiative, so it normally still is.
|
|
177
|
+
- **iPaaS flows** and **AI agents** have no snapshot at all — a rollback for
|
|
178
|
+
either is filling a real gap this tool owns, not routing around a platform
|
|
179
|
+
one. Each is reverted by diffing the *current* live state (re-read fresh,
|
|
180
|
+
since some steps may have landed) against the read taken right before this
|
|
181
|
+
apply touched it, direction flipped: current is baseline, the old
|
|
182
|
+
pre-apply state is the target. `buildFlowRevertPlan` / `planFlowRevert`
|
|
183
|
+
(commands/flows.ts) and `buildAgentRevertChangeSet` / `planAgentRevert`
|
|
184
|
+
(commands/agents.ts) — the agent diff is otherwise offline against the
|
|
185
|
+
local baseline (module doc comment there), so its revert point is a live
|
|
186
|
+
read this apply now takes specifically to have one, the same gap flows
|
|
187
|
+
closed first.
|
|
188
|
+
|
|
189
|
+
Rendered in full for every part that has anything to revert, before the one
|
|
190
|
+
question is asked, and executed — on confirmation — in the reverse of the
|
|
191
|
+
order they were applied: agents, then flows, then pipe structure.
|
|
192
|
+
|
|
193
|
+
None of this reaches further than the platform allows, and says so rather
|
|
194
|
+
than implying otherwise: no revert, Pipefy's or this tool's, un-sends an
|
|
195
|
+
email an automation already dispatched or un-fires a webhook that already
|
|
196
|
+
notified a third party. That is the platform's own irreversibility, not
|
|
197
|
+
something a bigger rollback would fix.
|
|
198
|
+
|
|
199
|
+
Verified live, for flows: a real ADD_ACTION rejection, the correct reverse
|
|
200
|
+
plan (delete what landed, restore the old link), and the flow back to its
|
|
201
|
+
exact original chain once applied — twice, once through the standalone
|
|
202
|
+
mechanism and once through the unified one that replaced it.
|
|
203
|
+
|
|
204
|
+
### Read the bytes back
|
|
205
|
+
|
|
206
|
+
The check nothing else can make. After a flow write, every stored datapill is
|
|
207
|
+
compared against what was sent. A migrating write path rewrites
|
|
208
|
+
`{{step_1['output'].x}}` into `{{step_1['output']['output'].x}}` — structurally
|
|
209
|
+
valid, accepted by Pipefy's own validator, and resolving to nothing at runtime.
|
|
210
|
+
|
|
211
|
+
Generalise the habit: when a write path might transform what you send, compare the
|
|
212
|
+
stored value, not the status code.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## 4. How this endpoint lies
|
|
217
|
+
|
|
218
|
+
`VALIDATION.md` is the full catalogue — 30 corrected API facts, 34 bugs found by
|
|
219
|
+
live running. Do not re-derive them. The shapes that bite most often:
|
|
220
|
+
|
|
221
|
+
| | |
|
|
222
|
+
| --- | --- |
|
|
223
|
+
| A mutation can **succeed and change nothing** | arguments with no input are dropped silently |
|
|
224
|
+
| `updatePipe.title_field_id` | wants the field's **uuid** or slug, never the numeric id the payload holds |
|
|
225
|
+
| `archiveField` | `{ uuid }`, not the id |
|
|
226
|
+
| `restoreRepoSnapshot` | **does not exist.** The endpoint has **`restoreRepoToSnapshot`**, which this build never looked for — so every apply printed "no one-command rollback" beside a snapshot that was restorable |
|
|
227
|
+
| `restoreRepoToSnapshot.versionId` | the **latest** snapshot only. An older one is refused with `Version_id must reference the latest uploaded snapshot of the Pipe`, so a pre-apply snapshot stops being restorable the moment the verify read-back snapshots the repo. Recovery is forward-first |
|
|
228
|
+
| a restore | **asynchronous**, reported through `operations` as `RESTORE`, not by the mutation returning |
|
|
229
|
+
| `pipe(id:)` on a database | **answers**, and returns its record statuses as phases. `Table.url` matching `/apollo_databases/` is the only discriminator |
|
|
230
|
+
| a database's snapshot payload | **is a pipe's** — the same twenty relation arrays, columns under `fields` with a real start-form `phase_id`. `createRepoSnapshot` accepts a table id |
|
|
231
|
+
| `updateTableField` / `deleteTableField` / `setTableFieldOrder` | the field's **slug**, with `table_id` alongside. The internal id and uuid are both refused — the inverse of `updatePhaseField` |
|
|
232
|
+
| `updateTable.summary_attributes[]` | the field's **internal id**, refusing the slug every neighbouring argument wants |
|
|
233
|
+
| `CreateTableFieldInput.type` | its description lists 23 values and **under-reports**: `dynamic_content` is accepted. `formula` is readable and refused. `FieldTypeId` is the authorable set |
|
|
234
|
+
| `createLabel` for a database | **`table_id`, not `pipe_id`** — the latter answers `Pipe not found with id: <the database id>`, which reads like a missing capability. Full CRUD once addressed right |
|
|
235
|
+
| `createWebhook` for a database | **accepted, and it attaches** — but the product offers no webhooks on a database and the actions are card-shaped, so one sits there inert. Refused deliberately, not by omission |
|
|
236
|
+
| a table field label | **unique per table** — a duplicate is refused with `Field label has already been taken`, so `label` is a sound natural key |
|
|
237
|
+
| a phase field label | `createPhaseField` **does not** enforce this — two fields both labelled "Classification" were created live, one in the same phase as the first, and the slug was disambiguated instead. `updatePhaseField.label` is non-null, though, so it resends the current label on *every* call regardless of what changed, and that is where a pre-existing duplicate surfaces as the same `Field label has already been taken` — caught offline now in `validate/schema.ts` |
|
|
238
|
+
| `deletePhaseField` | the field's **slug**, not the numeric id. A field has three identifiers and three mutations disagree: `updatePhaseField.id` takes the internal id, `archiveField` the uuid, this one the slug. Sent the id it says `Field not found with id: <id>` about a field that is plainly there |
|
|
239
|
+
| `phaseSettings` | reports **failure for changes it has already made** — three runs, three times. Its errors are inconclusive; re-read. |
|
|
240
|
+
| `AutomationActionParamsInput` | mixes snake and camel: `card_id`, `field_map`, but `taskParams`, `httpMethod` |
|
|
241
|
+
| pipe relation flags | `canCreateNewItems` in the mutation, `can_create_connected_cards` in the payload |
|
|
242
|
+
| `hidden_top_buttons` | a closed set of six, documented only in its own rejection |
|
|
243
|
+
| automation event/action params | a wrong one is rejected as **`is invalid`** and nothing else, on both endpoints. The catalogue (`automationEvents`, `automationActions`) is public — read it and refuse by name |
|
|
244
|
+
| an automation `condition` | **silently dropped** unless every expression carries `structure_id` — `expressions_structure` is a list of structure ids, not indices. Mutation succeeds, `expressions: []` stored |
|
|
245
|
+
| `createFieldCondition` without `condition` | **`"Something went wrong"`**. An unconditional hide is real and useful, but the key must be present as `{expressions: []}`. `condition` is not NON_NULL on the input type |
|
|
246
|
+
| `setFieldConditionOrder` | answers a **no-op with `"Something went wrong"`**. A new condition is appended by the server, so appending one in the workspace needs no reorder — compare against baseline + this phase's creates, not the baseline alone |
|
|
247
|
+
| `createFieldCondition.phaseId` | **authorized and then ignored.** A bogus id is refused with "Permission denied", a real one is accepted, and the condition lands on the start form regardless — even when both fields it names live on the phase asked for. The UI can target any phase; this mutation cannot |
|
|
248
|
+
| an expression with no `structure_id` | `Invalid input: Structure can't be blank`. `expressions_structure` names structure ids; derive one from position when the author gives none |
|
|
249
|
+
| a nested collection in verification | server-minted ids, an added `phase_id`, and `0` vs `"0"` in `expressions_structure` are **not drift**. Reduce the live side to what the intent states, at every depth, or a created condition can never verify |
|
|
250
|
+
| a field condition testing the field it hides | stored happily, and the **phase form stops opening**. The API validates none of this — not the self-reference, not the operation, not even `not_a_real_operation` |
|
|
251
|
+
| `automationVariables.active` | omitted for the life of the tool, so disabling an automation was a no-op that reported success |
|
|
252
|
+
| `updateAutomation` | works, across every field map dimension. The recipes' delete-and-recreate was inherited, not measured |
|
|
253
|
+
| iPaaS `IMPORT_FLOW` | migrates the tree it is given; never use it |
|
|
254
|
+
| iPaaS connections | the host marks any credential `ACTIVE` **without validating it** |
|
|
255
|
+
| `POST /app-connections/{id}` | accepts `scope` and `projectIds` and returns 200 ignoring both |
|
|
256
|
+
| iPaaS `ADD_ACTION`, parent is a `LOOP_ON_ITEMS` | `stepLocationRelativeToParent` is **required**, not optional, the moment the parent is a loop. Omitted (the shape every non-loop add uses, and the only shape ever probed before this) it answers `Loop step parent undefined not found` on the add that exits the loop — entering it (`INSIDE_LOOP`) already carried the field and already worked. `'AFTER'` sent explicitly fixes it |
|
|
257
|
+
| a flow datapill reading a `connector` field's value | **the connected card(s)' id(s) only** — never their own field values, at any depth you index into the payload. Reading a connected card's fields needs an explicit `getCardById` (or similar) step per id; the parent's connector-field datapill can never supply them |
|
|
258
|
+
|
|
259
|
+
### Closed sets: the list is published and enforced by nothing
|
|
260
|
+
|
|
261
|
+
The schema publishes every fixed list an authored value must come from —
|
|
262
|
+
`AutomationsEvents` (10), `AutomationsActions` (15), `FieldTypeId` (24), `Colors`
|
|
263
|
+
(12) — and then types the input fields that consume them as `ID` and `String`:
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
CreateAutomationInput.event_id ID
|
|
267
|
+
CreateAutomationInput.action_id ID
|
|
268
|
+
CreatePhaseFieldInput.type_id String
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
So GraphQL validates none of them. A wrong value passes query validation, reaches
|
|
272
|
+
a runtime check, and returns one of `is invalid` or `All fields must be filled
|
|
273
|
+
properly.` — naming no parameter, no field, and no accepted value, partway
|
|
274
|
+
through an apply.
|
|
275
|
+
|
|
276
|
+
The traps are near-misses, which is why reading the list beats knowing the domain.
|
|
277
|
+
All three of these were live:
|
|
278
|
+
|
|
279
|
+
| written | why it failed |
|
|
280
|
+
| --- | --- |
|
|
281
|
+
| `card_moved` + `event_params.in_phase_id` | real parameter, wrong event — it belongs to `card_inbox_received_email`. `card_moved` takes only `to_phase_id`. |
|
|
282
|
+
| `move_single_card` + `action_params.card_id` | real parameter, wrong action — accepted on `update_card_field`, refused here. Verified by probe. |
|
|
283
|
+
| a `time_range` field | in this tool's own hardcoded list; **not** a type the API offers |
|
|
284
|
+
|
|
285
|
+
**Never hardcode one of these lists.** That was the previous approach and it
|
|
286
|
+
rotted silently: `FIELD_TYPES` in `validate/schema.ts` carried `time_range`, which
|
|
287
|
+
does not exist, and omitted `dynamic_content`, which does — warning about a valid
|
|
288
|
+
type and passing an invalid one, with nothing to reveal either. It survives only
|
|
289
|
+
as a fallback for workspaces with no reference, and only as a warning.
|
|
290
|
+
|
|
291
|
+
Instead: `pipefy/reference.ts` reads the sets on every pull into
|
|
292
|
+
`.ppc/reference.json`, `validate/reference.ts` refuses offline and names the
|
|
293
|
+
value, and `REFERENCE.md` puts the same lists in the workspace so whoever edits
|
|
294
|
+
the JSON reads what is accepted instead of guessing. A new closed set is three
|
|
295
|
+
lines — add the enum to `TRACKED_ENUMS`, add a site to `ENUM_SITES`. **Absent
|
|
296
|
+
reference means the check is skipped, never that a value is valid.**
|
|
297
|
+
|
|
298
|
+
Pipefy's own docs for the automation mutations are worth reading once
|
|
299
|
+
([create](https://developers.pipefy.com/reference/automation-creation),
|
|
300
|
+
[update](https://developers.pipefy.com/reference/automation-update),
|
|
301
|
+
[delete](https://developers.pipefy.com/reference/automation-deletion),
|
|
302
|
+
[retrieve](https://developers.pipefy.com/reference/automation-retrieve)), but they
|
|
303
|
+
are secondary to the generated file: they describe the mutations, not which
|
|
304
|
+
parameters this pipe's events and actions accept, and they cannot go stale in a
|
|
305
|
+
way `pipe pull` would notice.
|
|
306
|
+
|
|
307
|
+
### Check the endpoint before assuming the internal API
|
|
308
|
+
|
|
309
|
+
All three automation mutations are **public** and were being sent to the internal
|
|
310
|
+
endpoint — inherited from the Change Migrator recipes and never re-checked, which
|
|
311
|
+
made automations the one entity written through an unversioned endpoint with no
|
|
312
|
+
introspection, for no reason. Before adding anything to `registry.ts` with
|
|
313
|
+
`transport: 'internal'`, introspect the public schema for it. If it genuinely is
|
|
314
|
+
not there, say which public mutation is missing and how that was verified.
|
|
315
|
+
|
|
316
|
+
### Probing is not free on this platform
|
|
317
|
+
|
|
318
|
+
Two separate incidents from the same wrong assumption — that schema validation
|
|
319
|
+
rejects a malformed request before anything acts:
|
|
320
|
+
|
|
321
|
+
- Enumerating the flow-operation enum with `request: {}` **published and enabled a
|
|
322
|
+
draft flow**. Five operations act on an empty request: `CHANGE_STATUS`,
|
|
323
|
+
`LOCK_AND_PUBLISH`, `LOCK_FLOW`, `CHANGE_FOLDER`, `UPDATE_METADATA`.
|
|
324
|
+
- Enumerating connection types with `"probe"` as the credential returned
|
|
325
|
+
**201 ACTIVE** every time, leaving a connection that would fail at runtime.
|
|
326
|
+
|
|
327
|
+
**Probe reads freely. Assume every write-shaped probe is real**, do it on a
|
|
328
|
+
throwaway object, and check the state afterwards. Record what you learn in
|
|
329
|
+
`VALIDATION.md`.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## 5. Where things live
|
|
334
|
+
|
|
335
|
+
```
|
|
336
|
+
src/
|
|
337
|
+
cli.ts command dispatch
|
|
338
|
+
config.ts endpoints, credential resolution, limits
|
|
339
|
+
pipefy/ graphql client · internal API · snapshots · reconstruction ·
|
|
340
|
+
discovery · capability detection · ipaas (Advanced Automations) ·
|
|
341
|
+
reference (the closed sets, read per pull)
|
|
342
|
+
codec/ unpack · pack · round-trip · flow (linked list <-> nodes)
|
|
343
|
+
workspace/ layout · read · write · lock · flows · stamp · generated docs
|
|
344
|
+
validate/ shape · referential integrity · closed sets, offline
|
|
345
|
+
diff/ pipe change set · flow change set · text render · HTML view
|
|
346
|
+
apply/ registry · mutation builders · compiler · id map · executor ·
|
|
347
|
+
flowops (iPaaS operations)
|
|
348
|
+
report/ run records · a pipe's integration surface
|
|
349
|
+
commands/ one file per command
|
|
350
|
+
test/ 116 tests, node:test, no framework
|
|
351
|
+
research/ probes and reads. The three that write are gated behind
|
|
352
|
+
PIPE_PROBE_WRITES=1 and say why.
|
|
353
|
+
.agents/skills/ four skills for agents editing workspaces
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**Docs, in the order they answer questions:**
|
|
357
|
+
|
|
358
|
+
| | |
|
|
359
|
+
| --- | --- |
|
|
360
|
+
| `README.md` | what the tool does, install, the loop, token costs, integrations |
|
|
361
|
+
| `VALIDATION.md` | every API fact and every bug found by running it live |
|
|
362
|
+
| `PLAN-IPAAS.md` | the iPaaS plan, its evidence, and what is still unbuilt |
|
|
363
|
+
| `PLAN-BEHAVIOUR.md` | **plan, unimplemented.** Why a valid, stored automation can be inert, and the layers that would catch it |
|
|
364
|
+
| `PLAN.md`, `PLAN-ROUTE-B.md`, `ROUTE-B-V2.md` | the original design and its revisions |
|
|
365
|
+
| `.agents/skills/*/SKILL.md` | how to *use* the tool on a workspace |
|
|
366
|
+
| a workspace's `REFERENCE.md` | generated, not written: what the API accepts in every closed-set position |
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## 6. Conventions
|
|
371
|
+
|
|
372
|
+
**Zero runtime dependencies, no build step.** Every import in `src/` is a `node:`
|
|
373
|
+
builtin. Node ≥ 22.18 strips the types itself. Verified by deleting
|
|
374
|
+
`node_modules` and running the CLI. Do not add a dependency; an FDE behind a proxy
|
|
375
|
+
that blocks the npm registry has to be able to clone and run.
|
|
376
|
+
|
|
377
|
+
`erasableSyntaxOnly` is on: **no constructor parameter properties, no enums, no
|
|
378
|
+
namespaces.** Declare fields explicitly.
|
|
379
|
+
|
|
380
|
+
**Comments explain why, not what.** The codebase is dense with them and they are
|
|
381
|
+
load-bearing: most record an API fact or a failure that motivated the code. When
|
|
382
|
+
you fix something subtle, leave the reason. When you read a comment that says an
|
|
383
|
+
argument is dropped or an error is inconclusive, believe it — it was measured.
|
|
384
|
+
|
|
385
|
+
**Tests are `node:test`, no framework.** `node --test test/*.test.ts`. Add a case
|
|
386
|
+
for the shape of a bug, not just the fix. `test/flow-codec.test.ts` and
|
|
387
|
+
`report.test.ts` are the models: they break things deliberately and assert the
|
|
388
|
+
finding.
|
|
389
|
+
|
|
390
|
+
**Commit messages describe the change and why, in prose.** They are long here on
|
|
391
|
+
purpose — a commit is where the reason for a subtle fix survives. Describe the
|
|
392
|
+
change, never the tooling that produced it.
|
|
393
|
+
|
|
394
|
+
**Line endings are mixed** (CRLF and LF). Multi-line string matching against
|
|
395
|
+
source files fails on the wrong one; match single lines or normalise first.
|
|
396
|
+
|
|
397
|
+
**`workspaces/` is gitignored** because a pulled pipe carries webhook URLs, which
|
|
398
|
+
are bearer-equivalent. Force-add only files you have checked, and say why.
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## 7. Things not to do
|
|
403
|
+
|
|
404
|
+
- **Do not delete-and-recreate to work around a missing write path.** It destroys
|
|
405
|
+
every existing value. `pipe plan` refuses these on purpose; say so and stop.
|
|
406
|
+
- **Do not enable an iPaaS flow as a side effect.** It arms the flow against live
|
|
407
|
+
traffic. `--allow-flow-enable` exists so it is always deliberate, and it is
|
|
408
|
+
additionally refused while the project holds a placeholder connection.
|
|
409
|
+
- **Do not write an iPaaS connection's credential.** Connections hold secrets:
|
|
410
|
+
read them, reference them, refuse to write them. A *placeholder* connection is
|
|
411
|
+
the exception and is tagged as such.
|
|
412
|
+
- **Do not send `IMPORT_FLOW`.** `IpaasClient.operate` refuses it. See §3.
|
|
413
|
+
- **Do not trust `ACTIVE`, a 200, or a validator.** All three have signed off on
|
|
414
|
+
broken states here.
|
|
415
|
+
- **Do not treat a missing prerequisite as a reason not to build.** A pipe with no
|
|
416
|
+
iPaaS connections gets a placeholder and the flow gets built; the structure is
|
|
417
|
+
what is being authored and it does not depend on the credential working.
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
## 8. What is still unbuilt
|
|
422
|
+
|
|
423
|
+
Honest gaps, so you do not promise them:
|
|
424
|
+
|
|
425
|
+
- **Check the endpoint before assuming the internal API.** Automations were sent
|
|
426
|
+
to the internal endpoint for the whole life of this project because the
|
|
427
|
+
Change Migrator recipes had to, and nobody re-checked. All three mutations are
|
|
428
|
+
public and documented. If something is on the internal API, there should be a
|
|
429
|
+
comment saying which public mutation is missing and how that was verified.
|
|
430
|
+
- ~~**Databases are discovered but not pulled.**~~ Done. A database is a repo:
|
|
431
|
+
its columns are fields on a start form and the phases after it are record
|
|
432
|
+
statuses, so the existing codec round-trips one with nothing added. What is
|
|
433
|
+
*not* done is records, excluded by decision rather than omission. Labels are
|
|
434
|
+
supported; webhooks are refused on purpose, with the measurement in the reason.
|
|
435
|
+
- **`setFieldConditionOrder`** works but is the least exercised write path.
|
|
436
|
+
- ~~**Flow node *creation* inside an existing flow** is refused by the compiler.~~
|
|
437
|
+
Done. `diff/flow.ts` attaches the authored tree to a `node.create` change so
|
|
438
|
+
`apply/flowops.ts` can find the new node's parent the same way
|
|
439
|
+
`buildFlowOperations` already does for a brand-new flow — whoever's
|
|
440
|
+
`_nextAction` / `_firstLoopAction` / a branch's `_children` names it — and
|
|
441
|
+
sends `ADD_ACTION`. A node nothing in the chain points to is still refused,
|
|
442
|
+
by name, rather than guessed at. Multiple new nodes in one apply are fine as
|
|
443
|
+
long as each is reachable once its own parent (new or old) exists, which the
|
|
444
|
+
file's execution-order-as-topological-order guarantee already provides.
|
|
445
|
+
- **`pipe validate` does not yet check flows.** The rules are specified in
|
|
446
|
+
`PLAN-IPAAS.md` §6 Phase 2 and prototyped in `research/ipaas/validate-flow.ts`
|
|
447
|
+
with 17 negative tests. Moving them into `src/validate/` is the next obvious
|
|
448
|
+
step, and it is where the cross-surface checks belong — a datapill naming a
|
|
449
|
+
field by slug that the pipe does not have, a write whose value cannot land in
|
|
450
|
+
that `type_id`, deleting a field a live flow depends on.
|
|
451
|
+
- **A desktop shell** is not coming. `PLAN.md` phase 3 described an Electron app;
|
|
452
|
+
it was removed rather than deferred, because `pipe diff --open` renders the
|
|
453
|
+
same HTML in the browser and a GUI is a second product to keep working. If a
|
|
454
|
+
UI is ever wanted, it belongs in its own repo consuming this one.
|
|
455
|
+
|
|
456
|
+
When you close one of these, update this section and `PLAN-IPAAS.md` together.
|