@uipath/maestro-builder-sdk 5.2.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.
Files changed (238) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -0
  3. package/dist/bindings-v2.d.ts +37 -0
  4. package/dist/bindings-v2.js +155 -0
  5. package/dist/bindings.d.ts +6 -0
  6. package/dist/bindings.js +6 -0
  7. package/dist/bpmn/bpmn-expr-check.d.ts +41 -0
  8. package/dist/bpmn/bpmn-expr-check.js +218 -0
  9. package/dist/bpmn/bpmn-sdk.d.ts +2257 -0
  10. package/dist/bpmn/bpmn-sdk.js +1476 -0
  11. package/dist/bpmn/check-cli.d.ts +3 -0
  12. package/dist/bpmn/check-cli.js +32 -0
  13. package/dist/bpmn/check.d.ts +25 -0
  14. package/dist/bpmn/check.js +430 -0
  15. package/dist/bpmn/compile-cli.d.ts +3 -0
  16. package/dist/bpmn/compile-cli.js +60 -0
  17. package/dist/bpmn/connector.d.ts +56 -0
  18. package/dist/bpmn/connector.js +199 -0
  19. package/dist/bpmn/decompile-cli.d.ts +8 -0
  20. package/dist/bpmn/decompile-cli.js +54 -0
  21. package/dist/bpmn/decompile.d.ts +6 -0
  22. package/dist/bpmn/decompile.js +1108 -0
  23. package/dist/bpmn/format-profile.d.ts +27 -0
  24. package/dist/bpmn/format-profile.js +11 -0
  25. package/dist/bpmn/intsvc.d.ts +77 -0
  26. package/dist/bpmn/intsvc.js +155 -0
  27. package/dist/bpmn/load.d.ts +4 -0
  28. package/dist/bpmn/load.js +22 -0
  29. package/dist/bpmn/merge-cli.d.ts +3 -0
  30. package/dist/bpmn/merge-cli.js +45 -0
  31. package/dist/bpmn/merge.d.ts +19 -0
  32. package/dist/bpmn/merge.js +293 -0
  33. package/dist/bpmn/registry-types.json +1869 -0
  34. package/dist/bpmn/serialize.d.ts +28 -0
  35. package/dist/bpmn/serialize.js +757 -0
  36. package/dist/bpmn/tidy-cli.d.ts +2 -0
  37. package/dist/bpmn/tidy-cli.js +39 -0
  38. package/dist/bpmn/typed-node.d.ts +382 -0
  39. package/dist/bpmn/typed-node.js +551 -0
  40. package/dist/bpmn/uipath-moddle.v1.json +823 -0
  41. package/dist/bpmn/xml-ranges.d.ts +45 -0
  42. package/dist/bpmn/xml-ranges.js +151 -0
  43. package/dist/case/case-expr-check.d.ts +35 -0
  44. package/dist/case/case-expr-check.js +85 -0
  45. package/dist/case/case-sdk.d.ts +1282 -0
  46. package/dist/case/case-sdk.js +923 -0
  47. package/dist/case/check-cli.d.ts +3 -0
  48. package/dist/case/check-cli.js +31 -0
  49. package/dist/case/check.d.ts +18 -0
  50. package/dist/case/check.js +456 -0
  51. package/dist/case/compile-cli.d.ts +3 -0
  52. package/dist/case/compile-cli.js +291 -0
  53. package/dist/case/decompile-cli.d.ts +9 -0
  54. package/dist/case/decompile-cli.js +117 -0
  55. package/dist/case/decompile.d.ts +187 -0
  56. package/dist/case/decompile.js +1170 -0
  57. package/dist/case/entry-points.d.ts +79 -0
  58. package/dist/case/entry-points.js +118 -0
  59. package/dist/case/format-profile.d.ts +60 -0
  60. package/dist/case/format-profile.js +79 -0
  61. package/dist/case/ids.d.ts +24 -0
  62. package/dist/case/ids.js +76 -0
  63. package/dist/case/load.d.ts +12 -0
  64. package/dist/case/load.js +32 -0
  65. package/dist/case/preserve.d.ts +133 -0
  66. package/dist/case/preserve.js +105 -0
  67. package/dist/case/serialize.d.ts +10 -0
  68. package/dist/case/serialize.js +1414 -0
  69. package/dist/check-cli.d.ts +3 -0
  70. package/dist/check-cli.js +118 -0
  71. package/dist/check.d.ts +146 -0
  72. package/dist/check.js +4898 -0
  73. package/dist/cli/commands/bpmn.d.ts +2 -0
  74. package/dist/cli/commands/bpmn.js +73 -0
  75. package/dist/cli/commands/case.d.ts +2 -0
  76. package/dist/cli/commands/case.js +60 -0
  77. package/dist/cli/commands/flow.d.ts +2 -0
  78. package/dist/cli/commands/flow.js +99 -0
  79. package/dist/cli/commands/registry.d.ts +10 -0
  80. package/dist/cli/commands/registry.js +170 -0
  81. package/dist/cli/common.d.ts +30 -0
  82. package/dist/cli/common.js +92 -0
  83. package/dist/cli/index.d.ts +2 -0
  84. package/dist/cli/index.js +18 -0
  85. package/dist/cli-run.d.ts +2 -0
  86. package/dist/cli-run.js +42 -0
  87. package/dist/compile-cli.d.ts +3 -0
  88. package/dist/compile-cli.js +160 -0
  89. package/dist/config.d.ts +47 -0
  90. package/dist/config.js +201 -0
  91. package/dist/core/actions.d.ts +3244 -0
  92. package/dist/core/actions.js +1062 -0
  93. package/dist/core/binding-messages.d.ts +1 -0
  94. package/dist/core/binding-messages.js +16 -0
  95. package/dist/core/bindings.d.ts +12 -0
  96. package/dist/core/bindings.js +67 -0
  97. package/dist/core/ceql.d.ts +73 -0
  98. package/dist/core/ceql.js +272 -0
  99. package/dist/core/cli-spelling.d.ts +39 -0
  100. package/dist/core/cli-spelling.js +62 -0
  101. package/dist/core/connector-raw-node.d.ts +21 -0
  102. package/dist/core/connector-raw-node.js +47 -0
  103. package/dist/core/connectors.d.ts +184 -0
  104. package/dist/core/connectors.js +128 -0
  105. package/dist/core/expr-check.d.ts +93 -0
  106. package/dist/core/expr-check.js +186 -0
  107. package/dist/core/expr.d.ts +323 -0
  108. package/dist/core/expr.js +400 -0
  109. package/dist/core/index.d.ts +10 -0
  110. package/dist/core/index.js +10 -0
  111. package/dist/core/library.d.ts +440 -0
  112. package/dist/core/library.js +1183 -0
  113. package/dist/core/list-envelope.d.ts +45 -0
  114. package/dist/core/list-envelope.js +97 -0
  115. package/dist/core/lookup-store.d.ts +22 -0
  116. package/dist/core/lookup-store.js +85 -0
  117. package/dist/core/lookups.d.ts +227 -0
  118. package/dist/core/lookups.js +231 -0
  119. package/dist/core/node-classes.d.ts +234 -0
  120. package/dist/core/node-classes.js +158 -0
  121. package/dist/core/script-return.d.ts +68 -0
  122. package/dist/core/script-return.js +197 -0
  123. package/dist/core/stable-id.d.ts +28 -0
  124. package/dist/core/stable-id.js +37 -0
  125. package/dist/core-definitions.json +16249 -0
  126. package/dist/decompile-cli.d.ts +3 -0
  127. package/dist/decompile-cli.js +131 -0
  128. package/dist/decompile.d.ts +138 -0
  129. package/dist/decompile.js +2482 -0
  130. package/dist/emit.d.ts +10 -0
  131. package/dist/emit.js +60 -0
  132. package/dist/event-filters.d.ts +130 -0
  133. package/dist/event-filters.js +137 -0
  134. package/dist/flow-expr-check.d.ts +17 -0
  135. package/dist/flow-expr-check.js +193 -0
  136. package/dist/flow-sdk.d.ts +1341 -0
  137. package/dist/flow-sdk.js +1164 -0
  138. package/dist/flow-variables.d.ts +13 -0
  139. package/dist/flow-variables.js +42 -0
  140. package/dist/format-profile.d.ts +72 -0
  141. package/dist/format-profile.js +97 -0
  142. package/dist/generators/_connections.py +287 -0
  143. package/dist/generators/_events.py +355 -0
  144. package/dist/generators/_lookups.py +114 -0
  145. package/dist/generators/_naming.py +185 -0
  146. package/dist/generators/_objects.py +299 -0
  147. package/dist/generators/_resolve.py +273 -0
  148. package/dist/generators/convert_library_to_md.py +1441 -0
  149. package/dist/generators/extract_library.py +125 -0
  150. package/dist/generators/generate_connectors_ts.py +936 -0
  151. package/dist/generators/generate_library_json.py +2527 -0
  152. package/dist/generators/generate_triggers_ts.py +110 -0
  153. package/dist/generators/materialize_registry.py +155 -0
  154. package/dist/generators/prepare_connector.py +1688 -0
  155. package/dist/index.d.ts +35 -0
  156. package/dist/index.js +35 -0
  157. package/dist/ixp-definition.json +1982 -0
  158. package/dist/library.d.ts +6 -0
  159. package/dist/library.js +6 -0
  160. package/dist/load.d.ts +16 -0
  161. package/dist/load.js +95 -0
  162. package/dist/merge-cli.d.ts +3 -0
  163. package/dist/merge-cli.js +47 -0
  164. package/dist/merge.d.ts +125 -0
  165. package/dist/merge.js +0 -0
  166. package/dist/migrate.d.ts +15 -0
  167. package/dist/migrate.js +245 -0
  168. package/dist/node-runtime.d.ts +3 -0
  169. package/dist/node-runtime.js +22 -0
  170. package/dist/package-name.d.ts +17 -0
  171. package/dist/package-name.js +17 -0
  172. package/dist/ref-resolve.d.ts +77 -0
  173. package/dist/ref-resolve.js +170 -0
  174. package/dist/registry/cache.d.ts +55 -0
  175. package/dist/registry/cache.js +134 -0
  176. package/dist/registry/curated-overlay.d.ts +16 -0
  177. package/dist/registry/curated-overlay.js +62 -0
  178. package/dist/registry/extractor.d.ts +22 -0
  179. package/dist/registry/extractor.js +18 -0
  180. package/dist/registry/materialize-bin.d.ts +2 -0
  181. package/dist/registry/materialize-bin.js +7 -0
  182. package/dist/registry/materialize-cli.d.ts +1 -0
  183. package/dist/registry/materialize-cli.js +46 -0
  184. package/dist/registry/materializer.d.ts +23 -0
  185. package/dist/registry/materializer.js +34 -0
  186. package/dist/registry/path-cli.d.ts +1 -0
  187. package/dist/registry/path-cli.js +56 -0
  188. package/dist/registry/prepare-cli.d.ts +62 -0
  189. package/dist/registry/prepare-cli.js +185 -0
  190. package/dist/registry/pull-cli.d.ts +1 -0
  191. package/dist/registry/pull-cli.js +274 -0
  192. package/dist/registry/python.d.ts +26 -0
  193. package/dist/registry/python.js +46 -0
  194. package/dist/registry/search-cli.d.ts +52 -0
  195. package/dist/registry/search-cli.js +159 -0
  196. package/dist/schema-version.d.ts +165 -0
  197. package/dist/schema-version.js +148 -0
  198. package/dist/serialize.d.ts +64 -0
  199. package/dist/serialize.js +6398 -0
  200. package/lib/README.md +112 -0
  201. package/lib/library-json.samples/index.json +158 -0
  202. package/lib/library-json.samples/invariant-collections.json +83 -0
  203. package/lib/library-json.samples/list-envelopes.json +123 -0
  204. package/lib/library-json.samples/uipath-atlassian-jira/create-issue@1.0.0.json +131 -0
  205. package/lib/library-json.samples/uipath-atlassian-jira/create-issue@1.0.0.v1def.json +306 -0
  206. package/lib/library-json.samples/uipath-atlassian-jira/generic/create_issue/create@1.0.0.json +86 -0
  207. package/lib/library-json.samples/uipath-atlassian-jira/generic/create_issue/create@1.0.0.v1def.json +208 -0
  208. package/lib/library-json.samples/uipath-atlassian-jira/get-issue@1.0.0.json +2040 -0
  209. package/lib/library-json.samples/uipath-atlassian-jira/get-issue@1.0.0.v1def.json +195 -0
  210. package/lib/library-json.samples/uipath-google-vertex/execute-google-vertex-agent@1.0.0.json +169 -0
  211. package/lib/library-json.samples/uipath-google-vertex/execute-google-vertex-agent@1.0.0.v1def.json +209 -0
  212. package/lib/library-json.samples/uipath-http-webhook/event-http-webhook@1.0.0.json +63 -0
  213. package/lib/library-json.samples/uipath-http-webhook/event-http-webhook@1.0.0.v1def.json +262 -0
  214. package/lib/library-json.samples/uipath-http-webhook/trigger-http-webhook@1.0.0.json +63 -0
  215. package/lib/library-json.samples/uipath-http-webhook/trigger-http-webhook@1.0.0.v1def.json +213 -0
  216. package/lib/library-json.samples/uipath-microsoft-azureactivedirectory/list-groups@1.0.0.json +227 -0
  217. package/lib/library-json.samples/uipath-microsoft-azureactivedirectory/list-groups@1.0.0.v1def.json +373 -0
  218. package/lib/library-json.samples/uipath-microsoft-onedrive/event-file-created@1.0.0.json +116 -0
  219. package/lib/library-json.samples/uipath-microsoft-onedrive/event-file-created@1.0.0.v1def.json +369 -0
  220. package/lib/library-json.samples/uipath-microsoft-onedrive/event-list-item-added@1.0.0.json +56 -0
  221. package/lib/library-json.samples/uipath-microsoft-onedrive/event-list-item-added@1.0.0.v1def.json +256 -0
  222. package/lib/library-json.samples/uipath-microsoft-onedrive/trigger-file-created@1.0.0.json +116 -0
  223. package/lib/library-json.samples/uipath-microsoft-onedrive/trigger-file-created@1.0.0.v1def.json +320 -0
  224. package/lib/library-json.samples/uipath-microsoft-onedrive/trigger-list-item-added@1.0.0.json +56 -0
  225. package/lib/library-json.samples/uipath-microsoft-onedrive/trigger-list-item-added@1.0.0.v1def.json +207 -0
  226. package/lib/library-json.samples/uipath-microsoft-outlook365/event-email-received@1.0.0.json +107 -0
  227. package/lib/library-json.samples/uipath-microsoft-outlook365/event-email-received@1.0.0.v1def.json +264 -0
  228. package/lib/library-json.samples/uipath-microsoft-outlook365/send-email@1.0.0.json +142 -0
  229. package/lib/library-json.samples/uipath-microsoft-outlook365/send-email@1.0.0.v1def.json +306 -0
  230. package/lib/library-json.samples/uipath-microsoft-outlook365/trigger-email-received@1.0.0.json +107 -0
  231. package/lib/library-json.samples/uipath-microsoft-outlook365/trigger-email-received@1.0.0.v1def.json +217 -0
  232. package/lib/library-json.samples/uipath-microsoft-powerautomate/invoke-a-microsoft-power-automate-flow@1.0.0.json +85 -0
  233. package/lib/library-json.samples/uipath-microsoft-powerautomate/invoke-a-microsoft-power-automate-flow@1.0.0.v1def.json +209 -0
  234. package/lib/library-json.samples/uipath-salesforce-slack/send-message-to-user@1.0.0.json +755 -0
  235. package/lib/library-json.samples/uipath-salesforce-slack/send-message-to-user@1.0.0.v1def.json +513 -0
  236. package/lib/library-json.samples/uipath-uipath-dataservice/query-entity-records@1.0.0.json +122 -0
  237. package/lib/library-json.samples/uipath-uipath-dataservice/query-entity-records@1.0.0.v1def.json +227 -0
  238. package/package.json +85 -0
@@ -0,0 +1,1441 @@
1
+ #!/usr/bin/env python3
2
+ """Convert the canonical JSON connector library into a markdown library.
3
+
4
+ Reads `<source>/index.json` and walks every entry — including the
5
+ `generic/<object>/<op>@<ver>.json` entries the Generic CRUD expansion
6
+ materializes — emitting a parallel tree of `.md` files plus a
7
+ sibling `<output>/index.json` whose `path` fields point at the new
8
+ `.md` files.
9
+
10
+ The output tree is the shape coding agents read for connector
11
+ discovery: header → connector / verb / object / HTTP → required
12
+ inputs → optional inputs → outputs. The authoring agent's connector
13
+ guidance (discovery pattern + Generic CRUD convention) lives in the main
14
+ SKILL.md, not here — the per-op files stay focused on schema.
15
+
16
+ Usage:
17
+ python3 convert_library_to_md.py --source <json-lib-dir> --output <md-lib-dir>
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import json
24
+ import re
25
+ import shutil
26
+ import sys
27
+ from pathlib import Path
28
+
29
+ # Symbol names cited below (`connector(SendMessageToUser, …)`, `.byEmail`) are
30
+ # DERIVED, not stored — and derived by the descriptor generator. Both import the
31
+ # same module so this page cannot cite a symbol that module did not emit.
32
+ from _naming import by_aliases, symbol_index
33
+ from _lookups import classify, load_invariant_collections, reference_of
34
+ # The EXAMPLE is type-checked against the generated descriptor, so its required
35
+ # set has to be the descriptor's, not this module's. The two vocabularies differ —
36
+ # see example_lines.
37
+ from generate_connectors_ts import (
38
+ input_fields as descriptor_input_fields,
39
+ is_container_part,
40
+ mark_lookup_fields as descriptor_mark_lookups,
41
+ )
42
+
43
+
44
+
45
+
46
+ def enum_value(v: object) -> str:
47
+ """One enum choice, as the literal an author would actually write.
48
+
49
+ The two schemas encode choices differently: ``inputSchema`` uses plain
50
+ strings (``"Prospect"``), while ``inputDefinition`` uses objects carrying a
51
+ display name alongside the wire value
52
+ (``{"name": "Abu Dhabi - Arabian Standard Time", "value": "Arabian Standard Time"}``).
53
+ Take ``value`` — that is what the flow has to send — so the rendering stays
54
+ identical across both shapes instead of leaking `{'value': …}` into the
55
+ catalog the agent reads.
56
+ """
57
+ if isinstance(v, dict):
58
+ return str(v.get("value", v.get("name", "")))
59
+ return str(v)
60
+
61
+
62
+ def reference_collection(f: dict) -> str:
63
+ """The collection to call to resolve a field's value, or "".
64
+
65
+ Prefer `reference.path` over `reference.objectName`: the schema documents
66
+ `path` as the one "used for making the call to get the reference details",
67
+ and the two genuinely disagree. Data Service `entityName` declares
68
+ `objectName: "system"` but is served from `/standard-objects`, so building
69
+ the call from `objectName` sends an author to a collection that is not the
70
+ one holding the records.
71
+
72
+ Either side can carry a filter (`curated_channels?types=...`), and
73
+ `uip is resources run list` takes the collection positionally with the
74
+ filter as `--query`, so the two halves are split here rather than left for
75
+ a reader to guess.
76
+ """
77
+ reference = f.get("reference")
78
+ if not isinstance(reference, dict):
79
+ return ""
80
+ source = reference.get("path") or reference.get("objectName") or ""
81
+ if not isinstance(source, str) or not source:
82
+ return ""
83
+ return source.split("?", 1)[0].strip("/")
84
+
85
+
86
+ def reference_query(f: dict) -> str:
87
+ """The filter half of whichever side `reference_collection` used, or ""."""
88
+ reference = f.get("reference")
89
+ if not isinstance(reference, dict):
90
+ return ""
91
+ source = reference.get("path") or reference.get("objectName") or ""
92
+ if not isinstance(source, str):
93
+ return ""
94
+ _, _, query = source.partition("?")
95
+ return query
96
+
97
+
98
+ def authoring_field(f: dict) -> tuple[str, str, str]:
99
+ """Return the source-facing name/type and any distinct wire field name.
100
+
101
+ A top-level ``users[*]`` declaration is flattened wire notation. The
102
+ generated TypeScript descriptor exposes it as ``users: string[]``. Showing
103
+ the raw name as a scalar led authors to pass a comma-delimited
104
+ ``'users[*]'`` value that compiled but was not a valid multiselect.
105
+ """
106
+ wire_name = str(f.get("name", "?"))
107
+ type_ = str(f.get("type", "?"))
108
+ if wire_name.endswith("[*]") and "." not in wire_name:
109
+ return wire_name[:-3], f"{type_}[]", wire_name
110
+ return wire_name, type_, ""
111
+
112
+
113
+ def enumeration_command(connector_key: str, f: dict) -> str:
114
+ """The `resources run list` call that shows what a field accepts.
115
+
116
+ Retained ONLY for identity lookups. For an ordinary lookup the helper
117
+ replaced this call, and leaving it on the page would offer two routes to the
118
+ same thing — the reader takes the one they already know, which is the manual
119
+ loop this whole surface exists to remove.
120
+
121
+ An identity lookup has no helper and no offline vocabulary (not one of them
122
+ carries an `enum`, measured across the shipped library), so removing the
123
+ command here would leave the field less documented than before. It is
124
+ reworded rather than deleted: not "how to resolve a name to an id" but "how
125
+ to see what this field accepts".
126
+ """
127
+ collection = reference_collection(f)
128
+ query = reference_query(f)
129
+ command = (
130
+ f"uip is resources run list {connector_key} {collection} "
131
+ f"--connection-id <id>"
132
+ )
133
+ if query:
134
+ command += f' --query "{query}"'
135
+ return command
136
+
137
+
138
+ def lookup_lines(
139
+ connector_key: str, action: str, fields: list[dict], symbol: str | None,
140
+ invariant: set,
141
+ ) -> list[str]:
142
+ """The `## Resolving lookups` body: helpers first, commands only where owed.
143
+
144
+ An author reading this page has to turn something a human said ("send it to
145
+ Dustin") into the id the API wants. The old answer here was a `resources run
146
+ list` command and a manual paging loop. The answer now is a helper call and
147
+ one `prepare`, because the id is the wrong thing to write down: it is
148
+ meaningless in review and silently wrong after a move to another tenant.
149
+
150
+ `symbol` is the generated descriptor's exported name, or None when the
151
+ operation has no descriptor (a Generic CRUD expansion) — in which case the
152
+ string form of `lookup()` is what the example shows.
153
+ """
154
+ resolvable = [(f, reference_of(f)) for f in fields
155
+ if classify(f, invariant) == "lookup"]
156
+ identity = [f for f in fields if classify(f, invariant) == "identity"]
157
+ if not resolvable and not identity:
158
+ return []
159
+
160
+ out: list[str] = []
161
+ if resolvable:
162
+ first_field, first_reference = resolvable[0]
163
+ # The lookup table is keyed by the WIRE name (`users[*]`), the same
164
+ # string `--resolve` and buildConnectorInputs match on. The table's
165
+ # `field` column keeps the authoring name, which is what the reader
166
+ # writes as an object key.
167
+ first_name = str(first_field.get("name") or authoring_field(first_field)[0])
168
+ first_names = [str(n) for n in (first_reference.get("lookupNames") or [])]
169
+ first_aliases = by_aliases(first_names)
170
+ # Without a descriptor the string form carries the real key AND action;
171
+ # a `<action>` placeholder here was a copy-paste trap, not a hint.
172
+ holder = symbol or f"'{connector_key}', '{action}'"
173
+ call = f"lookup({holder}, '{first_name}')"
174
+ sample = (f".{next(iter(first_aliases.values()))}('…')" if first_aliases
175
+ else f".by('{first_names[0]}', '…')" if first_names else ".by('id', '…')")
176
+ # The MECHANISM — resolve, never hand-write the wire value — is true of
177
+ # every lookup. The old JUSTIFICATION was not: it asserted the value is
178
+ # an opaque, connection-specific id, which is false for 320 of the
179
+ # library's 2,563 rows and, on `uipath-salesforce-slack`
180
+ # `send-message-to-user`, for a row printed three lines below the claim
181
+ # (`send_as` sends the word `bot`) — #706.
182
+ #
183
+ # Nothing in the metadata says which case a field is in: a reference
184
+ # block carries only childPath, dependsOn, filterPattern, hydration,
185
+ # lookupNames, lookupValue, objectName and path. Classifying would mean
186
+ # guessing from the SPELLING of `lookupValue`, a list that goes stale
187
+ # the next time a connector ships a handle nobody thought of. So the id
188
+ # claim is stated as the CONDITION it always was, and the `sends` column
189
+ # — already printed below — answers which case this page is.
190
+ out.append(
191
+ "These fields send a value the connector defines, not the label you "
192
+ "know it by — the `sends` column says which. Do not write that value "
193
+ "by hand: where it is an opaque id it is also connection-specific, so "
194
+ "a pasted one resolves to nobody (or to somebody else) against a "
195
+ "different connection. Resolve it late:"
196
+ )
197
+ out.append("")
198
+ out.append("```ts")
199
+ # A dotted wire name is a legal object key only when quoted.
200
+ # `buildConnectorInputs` accepts the flat form as well as the nested
201
+ # one, so this stays a one-line illustration rather than growing into a
202
+ # second copy of the example below.
203
+ key = first_name if _IDENT.fullmatch(first_name) else json.dumps(first_name)
204
+ out.append(f"{key}: {call}{sample}")
205
+ out.append("```")
206
+ out.append("")
207
+ out.append("| field | helpers | matches on | sends |")
208
+ out.append("| --- | --- | --- | --- |")
209
+ for f, reference in resolvable:
210
+ name, _, _ = authoring_field(f)
211
+ names = [str(n) for n in (reference.get("lookupNames") or [])]
212
+ aliases = by_aliases(names)
213
+ helper_cell = (" ".join(f"`.{a}(v)`" for a in aliases.values())
214
+ if aliases else "_(generic form only)_")
215
+ # `objectName` can carry a query string
216
+ # (`curated_channels?types=public_channel`). Show the collection.
217
+ target = (str(reference.get("objectName") or "").split("?", 1)[0]
218
+ or reference_collection(f))
219
+ out.append(
220
+ f"| `{name}` | {helper_cell} | "
221
+ + " ".join(f"`{n}`" for n in names)
222
+ + f" | `{target}.{reference.get('lookupValue')}` |"
223
+ )
224
+ out.append("")
225
+ out.append("Any field also takes the generic form: `.by('<name>', value)`.")
226
+ out.append("")
227
+ # ORDERING, not just the command. "Resolve before compiling" read as
228
+ # "resolve now", and agents ran this prepare before a line of source
229
+ # existed (observed across four sessions). The page's job is the
230
+ # loop: author the token, let check name the command, run it once.
231
+ out.append(
232
+ "Author the `lookup()` call and finish the flow — `check` reports "
233
+ "each token still unresolved with exactly this command; run it "
234
+ "once before compiling (`compile` refuses an unresolved lookup):"
235
+ )
236
+ out.append("")
237
+ # ONE command carrying every --resolve. `prepare` calls accumulate into
238
+ # the same overlay, but a page printing one command per field reads as
239
+ # one round-trip per field, and the whole point is that it is one.
240
+ out.append("```bash")
241
+ # NO --connection-id. `prepare` discovers the connection itself and
242
+ # writes both keys into bindings.json. Showing the flag is what kept
243
+ # agents listing connections by hand first — measured 3/3 runs after the
244
+ # discovery landed, because every documented example still had it.
245
+ out.append(
246
+ f"npx flow-sdk registry prepare {connector_key} {action} \\"
247
+ )
248
+ resolves = []
249
+ for f, reference in resolvable:
250
+ name, _, _ = authoring_field(f)
251
+ names = [str(n) for n in (reference.get("lookupNames") or [])] or ["id"]
252
+ resolves.append(f" --resolve {name}:{names[0]}=<value>")
253
+ out.extend(line + " \\" for line in resolves[:-1])
254
+ out.append(resolves[-1])
255
+ out.append("```")
256
+ out.append("")
257
+ strategies = {reference_strategy(r) for _, r in resolvable}
258
+ out.append(f"Strategy: {'; '.join(sorted(strategies))}.")
259
+ out.append("")
260
+
261
+ if identity:
262
+ out.append(
263
+ "**These fields take a plain string.** Their `reference` enumerates "
264
+ "the legal values rather than mapping a name to an id — what you "
265
+ "send back is what you searched for — so there is no helper and "
266
+ "nothing to resolve. The values exist only behind the connection, "
267
+ "so this is how to see what is accepted:"
268
+ )
269
+ out.append("")
270
+ out.append("```bash")
271
+ for f in identity:
272
+ name, _, _ = authoring_field(f)
273
+ out.append(f"# {name}")
274
+ out.append(enumeration_command(connector_key, f))
275
+ out.append("```")
276
+ out.append("")
277
+ return out
278
+
279
+
280
+ def reference_strategy(reference: dict) -> str:
281
+ """Prose for how `prepare` will retrieve this collection."""
282
+ if reference.get("dependsOn"):
283
+ depends = ", ".join(f"`{d}`" for d in reference["dependsOn"])
284
+ return f"dependent — resolve {depends} first"
285
+ if reference.get("filterPattern"):
286
+ return "one request (server-side filter)"
287
+ if reference.get("childPath"):
288
+ return "hierarchical walk"
289
+ return "paged scan (no server-side filter declared)"
290
+
291
+
292
+ def example_value(
293
+ f: dict, symbol: str | None, invariant: set,
294
+ connector_key: str = "", action: str = "",
295
+ ) -> str:
296
+ """The value to show for one required input.
297
+
298
+ Three rules, in order, and each is a decision rather than a detail:
299
+
300
+ * a resolvable lookup shows its helper — that is the whole point of the
301
+ helper existing, and an example that pasted an id would teach the opposite
302
+ of what `## Resolving lookups` says two sections above;
303
+ * an enum shows its FIRST DECLARED CHOICE, because that is a real legal
304
+ value and it shows the vocabulary at the same time;
305
+ * anything else shows `input('<name>')`, not a fake literal. A generated
306
+ `'<summary>'` compiles and is a lie; `input('summary')` compiles, is
307
+ idiomatic, and is honestly a placeholder. Copying it yields a flow that
308
+ declares the input rather than one carrying invented data.
309
+ """
310
+ name, _, _ = authoring_field(f)
311
+ # The lookup table is keyed by the WIRE name — `addLabelIds[*]`, not the
312
+ # authoring name `addLabelIds` — because that is what `--resolve` and
313
+ # `buildConnectorInputs` both match on. Passing the authoring name here is a
314
+ # type error at the `lookup(...)` call, which is how it was found.
315
+ wire = str(f.get("name") or name)
316
+ kind = classify(f, invariant)
317
+ if kind == "lookup":
318
+ reference = reference_of(f)
319
+ names = [str(n) for n in (reference.get("lookupNames") or [])]
320
+ aliases = by_aliases(names)
321
+ first = names[0] if names else "id"
322
+ if symbol:
323
+ if aliases:
324
+ return f"lookup({symbol}, '{wire}').{next(iter(aliases.values()))}('…')"
325
+ return f"lookup({symbol}, '{wire}').by('{first}', '…')"
326
+ # NO DESCRIPTOR — a Generic CRUD expansion. Its example is authored
327
+ # through the library by key/action/object, so the lookup has to be too,
328
+ # and the string form carries no typed aliases: only the generic `.by`.
329
+ # Emitting `lookup(<Descriptor>, …)` here was not a placeholder, it was
330
+ # a syntax error, on 614 pages.
331
+ return f"lookup('{connector_key}', '{action}', '{wire}').by('{first}', '…')"
332
+ type_ = str(f.get("type", "")).lower()
333
+ enum = f.get("enum")
334
+ if enum:
335
+ # The choice list is strings even when the field is not. Mailchimp's
336
+ # `email_type_option` declares `boolean` and offers "true"/"false", so
337
+ # emitting the choice verbatim produced `"true"` against a boolean.
338
+ # Where the choice cannot be typed to the declaration, fall back rather
339
+ # than emit something that does not compile.
340
+ choice = enum_value(enum[0])
341
+ if type_ in ("boolean", "bool"):
342
+ if choice.lower() in ("true", "false"):
343
+ return choice.lower()
344
+ elif type_ in ("number", "integer", "float", "double", "long"):
345
+ try:
346
+ float(choice)
347
+ except ValueError:
348
+ pass
349
+ else:
350
+ return choice
351
+ else:
352
+ return json.dumps(choice)
353
+ return f"input('{leaf_name(name)}')"
354
+
355
+
356
+ def leaf_name(field_name: str) -> str:
357
+ """`fields.project.key` -> `projectKey`; a readable input name, not a path."""
358
+ parts = [p for p in field_name.replace("[*]", "").split(".") if p]
359
+ if not parts:
360
+ return "value"
361
+ if len(parts) == 1:
362
+ return parts[0]
363
+ tail = parts[-2:]
364
+ head, *rest = tail
365
+ return head + "".join(w[:1].upper() + w[1:] for w in rest)
366
+
367
+
368
+ class _Node:
369
+ """One level of the example's input literal.
370
+
371
+ A segment can be BOTH an array and an object — `contacts[*].name` means a
372
+ list of objects — so array-ness is a flag on the node rather than a
373
+ different kind of node. Modelling it as "a list holding a value" is what an
374
+ earlier version did, and it crashed the moment a second field descended into
375
+ the same array.
376
+ """
377
+
378
+ __slots__ = ("children", "is_array", "leaf")
379
+
380
+ def __init__(self) -> None:
381
+ self.children: dict[str, _Node] = {}
382
+ self.is_array = False
383
+ self.leaf: str | None = None
384
+
385
+
386
+ def nest_inputs(pairs: list[tuple[str, str]]) -> list[str]:
387
+ """Render `{name: value}` pairs as the nested object literal an author writes.
388
+
389
+ `buildConnectorInputs` accepts either the flat dotted key or the nested
390
+ object, but the descriptor's generated `Inputs` type is nested — so an
391
+ example using flat keys would not typecheck against the very descriptor it
392
+ imports. Wildcards (`users[*]`) become arrays for the same reason.
393
+ """
394
+ root = _Node()
395
+ for name, value in pairs:
396
+ node = root
397
+ for segment in [p for p in str(name).split(".") if p]:
398
+ is_array = segment.endswith("[*]")
399
+ key = segment[:-3] if is_array else segment
400
+ child = node.children.get(key)
401
+ if child is None:
402
+ child = node.children[key] = _Node()
403
+ child.is_array = child.is_array or is_array
404
+ node = child
405
+ node.leaf = value
406
+
407
+ def render(node: _Node, indent: int) -> list[str]:
408
+ pad = " " * indent
409
+ lines: list[str] = []
410
+ for key, child in node.children.items():
411
+ safe = key if _IDENT.fullmatch(key) else json.dumps(key)
412
+ if child.children:
413
+ open_brace, close_brace = ("[{", "}],") if child.is_array else ("{", "},")
414
+ lines.append(f"{pad}{safe}: {open_brace}")
415
+ lines += render(child, indent + 1)
416
+ lines.append(f"{pad}{close_brace}")
417
+ else:
418
+ value = child.leaf if child.leaf is not None else "undefined"
419
+ lines.append(f"{pad}{safe}: [{value}]," if child.is_array
420
+ else f"{pad}{safe}: {value},")
421
+ return lines
422
+
423
+ return render(root, 1)
424
+
425
+
426
+ _IDENT = re.compile(r"[A-Za-z_$][A-Za-z0-9_$]*")
427
+
428
+
429
+ def example_lines(
430
+ j: dict, fields: list[dict], symbol: str | None, invariant: set,
431
+ v1def: dict | None = None,
432
+ ) -> list[str]:
433
+ """A compilable `connector(...)` call for this operation.
434
+
435
+ A page describes an operation's fields and never showed a call, so the agent
436
+ had to reconstitute the shape from a different document describing a general
437
+ form. The assembled answer is generatable from the same JSON, and the import
438
+ path is DERIVED rather than assumed: an operation whose schema arrives at
439
+ design time comes from `./connectors-local/` after a `prepare`, and saying
440
+ `./connectors/` for it would send the reader to a file that does not exist.
441
+
442
+ Required inputs only. The optional set runs to hundreds of fields on real
443
+ operations; an example showing them would not be an example.
444
+ """
445
+ connector_key = (j.get("connector") or {}).get("key") or "<connector>"
446
+ operation = j.get("operation") or {}
447
+ runtime = j.get("runtime") or {}
448
+ node_type = j.get("nodeType") or ""
449
+ action = node_type.rsplit(".", 1)[-1]
450
+ # WHICH REQUIRED SET. When a descriptor exists the example is type-checked
451
+ # against its `Inputs` interface, so the required set must be the one that
452
+ # interface declares — this module derives a different vocabulary, and the
453
+ # two disagree about required-ness on 4,529 of 7,036 operations (about the
454
+ # full field set on 6,164). Brevo `get-contact-by-email` is the shape of it:
455
+ # the page says "Required inputs: (none)" for an operation whose descriptor
456
+ # requires `email`, because the path parameter is picked up by one
457
+ # derivation and not the other.
458
+ #
459
+ # That disagreement is REAL and PRE-EXISTING, and reconciling it rewrites
460
+ # most of the markdown library — its own change, with its own review. Here
461
+ # the example simply follows the type it is checked against, so it compiles
462
+ # and is honest about what the descriptor wants.
463
+ #
464
+ # The string form takes `Record<string, unknown>`, so a generic example
465
+ # cannot fail on required-ness and keeps this module's vocabulary.
466
+ if symbol:
467
+ # v1def ships as either a dict or a one-element list; the descriptor
468
+ # generator normalises it the same way.
469
+ raw = v1def[0] if isinstance(v1def, list) and v1def else (v1def or {})
470
+ # Marked with the SAME call the descriptor generator uses, so the
471
+ # example puts a helper on exactly the fields that have one.
472
+ example_fields = descriptor_mark_lookups(
473
+ j, descriptor_input_fields(j, raw), invariant,
474
+ )
475
+ else:
476
+ example_fields = fields
477
+ required = [f for f in example_fields if f.get("required")]
478
+
479
+ schema_dynamic = bool(
480
+ operation.get("objectFromDiscovery")
481
+ or (operation.get("schemaAction") or {}).get("parents")
482
+ or runtime.get("requiresConnectionForSchema")
483
+ )
484
+ step = pascal_step(action)
485
+
486
+ if node_type.startswith(("uipath.connector.event.", "uipath.connector.trigger.")):
487
+ return event_example_lines(j, symbol, connector_key, action, step, v1def)
488
+
489
+ out = ["## Example", ""]
490
+ pairs = [(authoring_field(f)[0] if not authoring_field(f)[2] else f.get("name"),
491
+ example_value(f, symbol, invariant, connector_key, action))
492
+ for f in required]
493
+ # Import exactly what the rendered body uses. An example that references a
494
+ # helper it did not import does not compile, and a generated example that
495
+ # does not compile is worse than none: it is copied verbatim, and the reader
496
+ # spends turns working around a defect they did not author.
497
+ body_uses = {"connector"}
498
+ if any("lookup(" in v for _, v in pairs):
499
+ body_uses.add("lookup")
500
+ if any("input(" in v for _, v in pairs):
501
+ body_uses.add("input")
502
+ imported = ", ".join(sorted(body_uses))
503
+
504
+ out.append("```ts")
505
+ # THE PACKAGE, not a relative path. `@uipath/maestro-builder-sdk` is what an installed
506
+ # workspace resolves and what every shipped example uses; './sdk/flow-sdk.js'
507
+ # is a path that exists in no layout the docs describe, and an eval run
508
+ # caught an agent running `find / -iname flow-sdk.js` looking for it.
509
+ out.append(f"import {{ {imported} }} from '@uipath/maestro-builder-sdk';")
510
+ if symbol:
511
+ directory = "connectors-local" if schema_dynamic else "connectors"
512
+ out.append(f"import {{ {symbol} }} from './{directory}/{connector_key}.ts';")
513
+ out.append("")
514
+ head = f".step('{step}', connector({symbol}, {{"
515
+ else:
516
+ out.append("")
517
+ head = f".step('{step}', connector('{connector_key}', '{action}', {{"
518
+
519
+ out.append(head)
520
+ if pairs:
521
+ out += nest_inputs(pairs)
522
+ opts = []
523
+ if runtime.get("requiresConnection"):
524
+ opts.append(f"connection: '{connector_key.split('-')[-1]}'")
525
+ if runtime.get("requiresFolderKey"):
526
+ opts.append("folder: 'shared'")
527
+ if not symbol and operation.get("activityType") in ("Generic", "GenericExpanded"):
528
+ opts.append(f"object: '{operation.get('objectName') or '<object>'}'")
529
+ tail = "}" + (f", {{ {', '.join(opts)} }}" if opts else "") + "))"
530
+ out.append(tail)
531
+ out.append("```")
532
+ out.append("")
533
+ if schema_dynamic and symbol:
534
+ # ONE ordering per page. This trailer used to say "run prepare FIRST;
535
+ # the import does not resolve until you do" — which read as an
536
+ # instruction to prepare before authoring, on the same page whose
537
+ # header says author first. Two orderings on one page, and the agent
538
+ # takes the one it read last (observed: an agent front-loaded prepare
539
+ # for exactly this sentence). The import fact stays; the ordering
540
+ # instruction becomes the author-first contract.
541
+ if operation.get("objectFromDiscovery"):
542
+ out.append(
543
+ f"The descriptor comes from `./connectors-local/` and this "
544
+ f"import resolves only after "
545
+ f"`npx flow-sdk registry prepare {connector_key} {action} "
546
+ f"--object <name>` runs. That never blocks authoring: write "
547
+ f"the step first with the string form and "
548
+ f"`{{ object: '<name-as-the-task-said-it>' }}` — `check` "
549
+ f"names this exact prepare — then switch to this import."
550
+ )
551
+ else:
552
+ out.append(
553
+ f"The descriptor comes from `./connectors-local/` and this "
554
+ f"import resolves only after "
555
+ f"`npx flow-sdk registry prepare {connector_key} {action}` "
556
+ f"runs. That never blocks authoring: the fields in this "
557
+ f"catalog are authorable immediately, and `check` flags any "
558
+ f"input outside them with this same command."
559
+ )
560
+ out.append("")
561
+ return out
562
+
563
+
564
+ def event_filter_lines(j: dict, v1def: dict | None) -> list[str]:
565
+ """The fields a subscription may filter on — and how complete that list is.
566
+
567
+ Without this the page named the event's PARAMETERS and stopped, so the one
568
+ thing an author needs in order to write `filters` was the one thing the
569
+ page did not say — while `check` refuses a leaf outside the vocabulary,
570
+ because the platform silently DROPS an unmatched one and the deployed
571
+ trigger then fires on everything.
572
+
573
+ The completeness caveat is the other half. Where a required query/path
574
+ parameter selects the schema (Jira's `project` / `issuetype`), the fields
575
+ below are what the connector reports with no values supplied, and no
576
+ discovery call accepts values — so that list is a baseline the real one
577
+ extends, `check` warns rather than refuses there, and the page has to say
578
+ so instead of implying the list is exhaustive.
579
+
580
+ TWO SOURCES, matching how the platform builds each kind of list — see
581
+ `Library.eventFilterFields`, which this page must agree with or `check`
582
+ refuses a leaf the page advertised. A CURATED event filters on the fields
583
+ its definition marks (`order: 1` / `isCuratedEventField`), which arrive as
584
+ `v1def.filterFields`; a GENERIC event filters on every field the object
585
+ supports GET on, which arrive as the entry's `outputSchema`. Reading the
586
+ marked list for both is what made Jira `record-created` advertise two
587
+ filterable fields where the designer offers 258.
588
+ """
589
+ node_type = j.get("nodeType", "")
590
+ if not node_type.startswith(("uipath.connector.event.", "uipath.connector.trigger.")):
591
+ return []
592
+ generic = (j.get("operation") or {}).get("activityType") == "GenericTrigger"
593
+ fields = list(((v1def or {}).get("filterFields") or {}).get("fields") or [])
594
+ if generic:
595
+ fields += (j.get("outputSchema") or {}).get("fields") or []
596
+ names: list[str] = []
597
+ for f in fields:
598
+ name = f.get("name")
599
+ if isinstance(name, str) and name and name not in names:
600
+ names.append(name)
601
+ if not names:
602
+ return []
603
+ selectors = [
604
+ f["name"] for f in ((v1def or {}).get("eventParameters") or {}).get("fields") or []
605
+ if f.get("required") and f.get("bucket") in ("query", "path")
606
+ ]
607
+ out = ["## Filtering", ""]
608
+ out.append(
609
+ "Narrow which records fire the subscription with `filters`. Each leaf is "
610
+ "`{ field, <operator>: value }` with exactly ONE operator "
611
+ "(`contains`, `startsWith`, `endsWith`, `equals`, `notEquals`, "
612
+ "`lessThan`, `lessThanOrEqual`, `greaterThan`, `greaterThanOrEqual`):"
613
+ )
614
+ out.append("")
615
+ out.append("```ts")
616
+ out.append(f"filters: [{{ field: '{names[0]}', contains: '<value>' }}]")
617
+ out.append("```")
618
+ out.append("")
619
+ out.append(
620
+ "Field names are CASE-SENSITIVE and only these are filterable — the "
621
+ "platform drops a leaf it does not recognise and the trigger then fires "
622
+ "on records the filter should have excluded, so `check` reports one that "
623
+ "is not here."
624
+ )
625
+ out.append("")
626
+ shown = ", ".join(f"`{n}`" for n in names[:40])
627
+ more = f" …and {len(names) - 40} more" if len(names) > 40 else ""
628
+ out.append(f"{shown}{more}")
629
+ out.append("")
630
+ if selectors:
631
+ listed = ", ".join(f"`{n}`" for n in selectors)
632
+ out.append(
633
+ f"> **This list is a baseline, not the whole vocabulary.** {listed} "
634
+ f"select this event's schema, and the connector derives its filter "
635
+ f"and output fields from their VALUES — which no discovery call "
636
+ f"accepts, so the fields above are the ones reported with none "
637
+ f"supplied. A name outside them may still be legal, which is why "
638
+ f"`check` warns rather than refuses here. Confirm it against the "
639
+ f"trigger in the designer before deploying."
640
+ )
641
+ out.append("")
642
+ elif any(
643
+ value for name, value in
644
+ (((j.get("connector") or {}).get("discovery") or {}).items()) if name != "type"
645
+ ):
646
+ # Distinct from the selector caveat above, and from the activity story:
647
+ # `resources describe` takes `-f`/`--action`, so preparing an ACTIVITY
648
+ # materializes its connection-scoped fields. `uip is triggers describe`
649
+ # takes neither, so for an event there is no call that reaches them and
650
+ # the list stays short however thorough the author is. Keyed on ANY
651
+ # discovery flag to match `Library.eventParameterFields.filtersPartial`
652
+ # — jira is classified `static` with `hasFieldDiscovery: true`, and it
653
+ # is the connector we have actually measured growing.
654
+ out.append(
655
+ "> **This list is a subset no `prepare` can complete.** This connector "
656
+ "reads its schema from the connection, and the trigger metadata call "
657
+ "takes no parent values — `uip is triggers describe` has no `-f`, the "
658
+ "flag that materializes an ACTIVITY's connection-scoped fields. So a "
659
+ "name outside the list above may be a real field of yours; `check` warns "
660
+ "rather than refuses, and the designer is where to confirm it."
661
+ )
662
+ out.append("")
663
+ return out
664
+
665
+
666
+ def event_example_lines(
667
+ j: dict, symbol: str | None, connector_key: str, action: str, step: str,
668
+ v1def: dict | None = None,
669
+ ) -> list[str]:
670
+ """A compilable `onEvent(...)` / `waitForEvent(...)` call for a connector EVENT.
671
+
672
+ An event is not an action: its descriptor is a `triggerDescriptor()` that
673
+ `onEvent`/`waitForEvent` take, and `connector(...)` does not accept it. A
674
+ GENERIC event (`activityType: GenericTrigger`) watches ONE object of the
675
+ connection and its entry names none, so the example shows the `object`
676
+ option that names it; a curated event has its object built in, so the
677
+ example shows `where` with the event parameters the entry declares.
678
+ """
679
+ operation = j.get("operation") or {}
680
+ generic = operation.get("activityType") == "GenericTrigger"
681
+ start = node_type_is_start = j.get("nodeType", "").startswith("uipath.connector.trigger.")
682
+ # The SIDECAR's event parameters, for the same reason the typed `where`
683
+ # reads them: an entry nothing has described still carries the node's raw
684
+ # input definition, whose names (`filter.filters[*].id`) are not even legal
685
+ # object keys — the example stopped parsing when it was built from those.
686
+ # No parameters, no `where` in the example.
687
+ params = [
688
+ f["name"] for f in ((v1def or {}).get("eventParameters") or {}).get("fields") or []
689
+ if isinstance(f.get("name"), str) and not f.get("hidden")
690
+ ]
691
+ factory = "onEvent" if start else "waitForEvent"
692
+ out = ["## Example", "", "```ts"]
693
+ out.append(f"import {{ {factory} }} from '@uipath/maestro-builder-sdk';")
694
+ if symbol:
695
+ out.append(f"import {{ {symbol} }} from './connectors/{connector_key}.ts';")
696
+ out.append("")
697
+ opts = []
698
+ where_lines: list[str] = []
699
+ if generic:
700
+ opts.append("object: '<Object>'")
701
+ elif params:
702
+ # NESTED, exactly as inputs are: the generated `…Where` interface splits a
703
+ # dotted parameter into an object (`channelData.channel.id` becomes
704
+ # `channelData: ConnectorValue<ChannelDataInput>`), so a flat key — quoted
705
+ # or not — is not a member of the type. Unnested, Teams'
706
+ # `channelData.channel.id` made the only two event examples in the library
707
+ # the only `## Example` fences in ~8,000 that did not parse.
708
+ where_lines = [" " + line for line in nest_inputs([(p, f"'<{p}>'") for p in params])]
709
+ opts.append(f"connection: '{connector_key.split('-')[-1]}'")
710
+ opts.append("folder: 'shared'")
711
+ lead = f"{factory}({symbol}, {{" if symbol else (
712
+ f"{factory}({{ connector: '{connector_key}', event: '{action}',"
713
+ )
714
+ if where_lines:
715
+ # Multi-line, like the action example, because the nested `where` is.
716
+ out.append(f".trigger({lead}" if start else f".step('{step}', {lead}")
717
+ out.append(" where: {")
718
+ out += where_lines
719
+ out.append(" },")
720
+ for option in opts:
721
+ out.append(f" {option},")
722
+ out.append("}))")
723
+ else:
724
+ call = f"{factory}({symbol}, {{ {', '.join(opts)} }})" if symbol else (
725
+ f"{factory}({{ connector: '{connector_key}', event: '{action}', {', '.join(opts)} }})"
726
+ )
727
+ out.append(f".trigger({call})" if start else f".step('{step}', {call})")
728
+ out.append("```")
729
+ out.append("")
730
+ if generic:
731
+ out.append(
732
+ f"A GENERIC event: it fires for ONE object of the connection, so `object` "
733
+ f"names it. `uip is triggers objects {connector_key} "
734
+ f"{operation.get('eventOperation') or operation.get('name') or '<EVENT>'} "
735
+ f"--connection-id <id>` lists the objects the bound connection exposes."
736
+ )
737
+ out.append("")
738
+ del node_type_is_start
739
+ return out
740
+
741
+
742
+ def pascal_step(action: str) -> str:
743
+ """`send-message-to-user` -> `sendMessageToUser`, a usable step name."""
744
+ words = [w for w in action.replace("_", "-").split("-") if w]
745
+ if not words:
746
+ return "step"
747
+ head, *rest = words
748
+ return head + "".join(w[:1].upper() + w[1:] for w in rest)
749
+
750
+
751
+ def schema_action_lines(
752
+ connector_key: str, object_name: str, verb: str,
753
+ action: dict, fields: list[dict],
754
+ ) -> list[str]:
755
+ """How to get an operation's real field set when values select it.
756
+
757
+ Jira `create-issue` describes to 2 fields with nothing supplied and 32 once
758
+ a project and an issue type are given; a different issue type in the same
759
+ project gives a different set again. Salesforce SOQL is the same mechanism
760
+ driven by the query text. Without this section the page reads as an
761
+ operation that takes two inputs.
762
+ """
763
+ parents = action.get("parents") or []
764
+ if not parents:
765
+ return []
766
+ by_name = {f.get("name"): f for f in fields}
767
+ trigger = action.get("trigger")
768
+ replaces = action.get("replaces")
769
+ obj = object_name or "<object>"
770
+
771
+ out = ["## Resolving the field set", ""]
772
+ out.append(
773
+ "The inputs listed below are only the ones that SELECT the field set. "
774
+ "The real set depends on the values of "
775
+ + ", ".join(f"`{p}`" for p in parents)
776
+ + " — a different value gives a different set, so there is no single "
777
+ "field list for this operation."
778
+ )
779
+ out.append("")
780
+ if trigger:
781
+ out.append(
782
+ f"It is not automatic: `{trigger}` is a trigger rather than data, "
783
+ "so the schema has to be requested once the values are in place."
784
+ )
785
+ out.append("")
786
+ out.append("Resolve the parents in this order, then describe again:")
787
+ out.append("")
788
+
789
+ for position, parent in enumerate(parents, start=1):
790
+ reference = (by_name.get(parent) or {}).get("reference") or {}
791
+ collection = reference_collection(by_name.get(parent) or {})
792
+ if not collection:
793
+ out.append(
794
+ f"{position}. `{parent}` — supply the value; it is not chosen "
795
+ "from a list."
796
+ )
797
+ out.append("")
798
+ continue
799
+ query = reference_query(by_name.get(parent) or {})
800
+ pattern = reference.get("filterPattern") or ""
801
+ names = reference.get("lookupNames") or []
802
+ send = reference.get("lookupValue") or "id"
803
+ out.append(
804
+ f"{position}. `{parent}` — from `{collection}`"
805
+ + (f", match on {' or '.join(f'`{n}`' for n in names)}" if names
806
+ else "")
807
+ + f", send `{send}`."
808
+ )
809
+ # A placeholder means an earlier parent's value goes here.
810
+ if "{" in collection:
811
+ out.append(
812
+ " Substitute the value resolved above for the "
813
+ "`{…}` part of the collection."
814
+ )
815
+ out.append("")
816
+ out.append(" ```bash")
817
+ command = (
818
+ f" uip is resources run list {connector_key} {collection} "
819
+ f"--connection-id <id> \\"
820
+ )
821
+ out.append(command)
822
+ if pattern:
823
+ # The connector declares its own search. Substituting the
824
+ # wanted term narrows server-side and usually removes the
825
+ # paging: Jira project goes from 420+ rows across pages to 2
826
+ # in one call. It only works against the path the reference
827
+ # declares (`project/search`), not the plain object.
828
+ out.append(
829
+ ' --query "'
830
+ + pattern.replace("{filter}", "<search-term>")
831
+ + '" \\'
832
+ )
833
+ elif query:
834
+ out.append(f' --query "{query}" \\')
835
+ projection = ",".join(
836
+ f"{n.split('[')[0]}:{n}" for n in (names[:2] or ["name"])
837
+ )
838
+ out.append(
839
+ f' --output-filter "{{items: items[*].{{{projection},'
840
+ f"send:{send}}}, page: Pagination.{{h:HasMore,t:NextPageToken}}}}\" \\"
841
+ )
842
+ out.append(" --output json")
843
+ out.append(" ```")
844
+ out.append("")
845
+ if pattern:
846
+ out.append(
847
+ " The connector declares its own search, so narrow with "
848
+ "the term the user gave rather than reading pages: one "
849
+ "call usually returns a handful, and `page` comes back "
850
+ "either absent or with `h` as `\"false\"`. Page only if "
851
+ "the term matches nothing."
852
+ )
853
+ else:
854
+ out.append(
855
+ " Page it: `page.h` is the `elements-has-more` response "
856
+ "header verbatim, so it is the string `\"true\"` rather "
857
+ "than a boolean, and `page` is absent altogether when the "
858
+ "connector sends no pagination headers. While it reads "
859
+ "`\"true\"` and the value has not appeared, pass `page.t` "
860
+ "back as `--query \"nextPage=<token>\"` and read the next "
861
+ "page. Stop on the match. The projection keeps each page "
862
+ "small but does not reduce the call count."
863
+ )
864
+ out.append("")
865
+ out.append(
866
+ " Before paging, check this field's `Reference` in the "
867
+ "`describe` output for a `FilterPattern`: it is "
868
+ "connection-scoped, so it is not in this catalog, but when "
869
+ "present it is the connector's own search and removes the "
870
+ "paging — Jira's project lookup declares `query={filter}`, "
871
+ "which turns 420-plus rows across pages into two in one call. "
872
+ "Substitute the wanted term into whatever pattern this field "
873
+ "declares; the shape is:"
874
+ )
875
+ out.append("")
876
+ out.append(" ```bash")
877
+ out.append(
878
+ f" uip is resources run list {connector_key} {collection} "
879
+ f"--connection-id <id> \\"
880
+ )
881
+ out.append(
882
+ ' --query "<filter-pattern with the term substituted>" '
883
+ "--output json"
884
+ )
885
+ out.append(" ```")
886
+ out.append("")
887
+
888
+ supplied = " ".join(f"-f {p}=<value>" for p in parents)
889
+ out.append(f"{len(parents) + 1}. Describe again with the resolved values:")
890
+ out.append("")
891
+ out.append(" ```bash")
892
+ out.append(
893
+ f" uip is resources describe {connector_key} {obj} "
894
+ f"--operation {verb} \\"
895
+ )
896
+ out.append(f" --connection-id <id> {supplied} --output json")
897
+ out.append(" ```")
898
+ out.append("")
899
+ if replaces == "output":
900
+ out.append(
901
+ " This replaces the **response** fields — it tells you the shape "
902
+ "of what comes back, not what to send."
903
+ )
904
+ elif replaces == "both":
905
+ out.append(
906
+ " This replaces the **request** fields AND the **response** "
907
+ "fields — both halves of this operation change with the values "
908
+ "above, so read the response shape from the same call."
909
+ )
910
+ else:
911
+ out.append(" This replaces the **request** fields.")
912
+ out.append("")
913
+ # Only meaningful where the action rewrites the INPUT. Where it rewrites
914
+ # the output — a SOQL query, Jira `get-issue` — a resolved call leaves the
915
+ # request fields as exactly the parents, so telling a reader to treat that
916
+ # as failure makes every success look broken. Matches the gate in the CLI
917
+ # (`isParentOnlyResult`, UiPath/cli#3773).
918
+ if replaces in ("input", "both"):
919
+ out.append(
920
+ "If the result contains only the parents again, one of the values "
921
+ "does not exist on this connection. The call reports `Success` "
922
+ "either way, so the count alone cannot tell that apart from having "
923
+ "supplied nothing — check whether anything beyond the parents came "
924
+ "back. Too FEW parents fails loudly instead, with "
925
+ "`No api-type ObjectAction matched`."
926
+ )
927
+ out.append("")
928
+ return out
929
+
930
+
931
+ def field_line(f: dict) -> str:
932
+ name, type_, wire_name = authoring_field(f)
933
+ desc = (f.get("description") or "").replace("\n", " ").strip()
934
+ enum = f.get("enum")
935
+ suffix = f", enum: {' | '.join(enum_value(e) for e in enum)}" if enum else ""
936
+ if wire_name:
937
+ suffix += f", wire field: `{wire_name}`"
938
+ # The wire slot, for a QUERY, PATH, or MULTIPART parameter. Body fields stay
939
+ # untagged so every existing line is byte-identical: `location` is the new
940
+ # information, and a field with none has always been a body field.
941
+ loc = f.get("location")
942
+ if loc in ("query", "path", "multipart"):
943
+ suffix += f", {loc}"
944
+ # Name the collection so the line itself says "this is a lookup, not free
945
+ # text". `Resolving lookups` below carries the call that resolves it.
946
+ collection = reference_collection(f)
947
+ if collection:
948
+ suffix += f", lookup: {collection}"
949
+ head = f"- `{name}` ({type_}{suffix})"
950
+ return f"{head} — {desc}" if desc else head
951
+
952
+
953
+ # `operation.parameters[].type` → the wire slot, mirroring
954
+ # `typescript/sdk/src/core/library.ts`'s PARAM_LOCATIONS.
955
+ PARAM_LOCATIONS = {
956
+ "query": "query", "path": "path", "multipart": "multipart"
957
+ }
958
+
959
+ # A parameter's `dataType` → the scalar names `compile`'s literal type-check
960
+ # understands (library.ts's `scalarType`).
961
+ _SCALARS = {
962
+ "integer": "number", "number": "number", "double": "number",
963
+ "float": "number", "long": "number",
964
+ "boolean": "boolean", "string": "string",
965
+ }
966
+
967
+
968
+ def scalar_type(data_type: object) -> str:
969
+ return _SCALARS.get(data_type, data_type if isinstance(data_type, str) else "?")
970
+
971
+
972
+ def param_fields(j: dict) -> list[dict]:
973
+ """The op's QUERY, PATH, and MULTIPART parameters as body-shaped fields.
974
+
975
+ These are inputs the COMPILER accepts — `core/library.ts`'s `readFields`
976
+ unions them with the v1def's body fields and tags each with its `location`,
977
+ which is what `serialize` routes on (flow-builder-sdk#132). The markdown was
978
+ written before that, so it documented the body half only.
979
+
980
+ That gap was large and, worse, silently wrong about REQUIREDNESS. Measured on
981
+ the shipped library (7,030 entries, 2026-07-31):
982
+
983
+ | | count |
984
+ |---|--:|
985
+ | entries carrying `operation.parameters` | **5,687** (81%) |
986
+ | parameters dropped | **14,872** (10,710 query · 4,162 path) |
987
+ | entries with a REQUIRED parameter | **4,413** (5,563 required params) |
988
+
989
+ Every one of those 4,413 rendered *"Required inputs: _(none)_"* — including
990
+ Jira `get-issue`, whose `{issueId}` path parameter the operation cannot run
991
+ without, and Data Service `query-entity-records`, whose required `entityName`
992
+ picks the entity. The tasks that used those ops named the parameters in their
993
+ own prompts, which is why nothing surfaced it.
994
+ """
995
+ out = []
996
+ for p in (j.get("operation") or {}).get("parameters") or []:
997
+ name = p.get("name")
998
+ loc = PARAM_LOCATIONS.get(p.get("type"))
999
+ if not isinstance(name, str) or not name or not loc:
1000
+ continue
1001
+ # A string-typed multipart part is the transport container the runtime
1002
+ # composes from the body fields — documenting it as the REQUIRED input
1003
+ # beside the real `body.content` is exactly what sent authors down
1004
+ # `body: 'text'` (value ignored at dispatch) and `'body.content'`
1005
+ # (refused by compile) on 2026-09-01. Only file parts are inputs.
1006
+ if is_container_part(p):
1007
+ continue
1008
+ f = {"name": name, "type": scalar_type(p.get("dataType")),
1009
+ "required": p.get("required") is True, "location": loc}
1010
+ if p.get("description"):
1011
+ f["description"] = p["description"]
1012
+ if p.get("displayName"):
1013
+ f["displayName"] = p["displayName"]
1014
+ # `reference` says the value is an id resolved from a collection, not
1015
+ # free text. Without it the page reads as "type anything here", which
1016
+ # is how an author ends up sending a channel NAME where the API wants
1017
+ # a channel id.
1018
+ if p.get("reference"):
1019
+ f["reference"] = p["reference"]
1020
+ # A parameter's `enum` is deprecated in favour of `enhancedEnum`; the
1021
+ # latter is what connectors actually populate.
1022
+ if p.get("enum"):
1023
+ f["enum"] = p["enum"]
1024
+ elif p.get("enhancedEnum"):
1025
+ f["enum"] = p["enhancedEnum"]
1026
+ out.append(f)
1027
+ return out
1028
+
1029
+
1030
+ def catalog_multipart_fields(j: dict) -> list[dict]:
1031
+ """File fields whose older catalog entry omitted the multipart parameter."""
1032
+ out = []
1033
+ for field in (j.get("inputSchema") or {}).get("fields") or []:
1034
+ if (
1035
+ not field.get("name")
1036
+ or str(field.get("type") or "").lower() != "file"
1037
+ ):
1038
+ continue
1039
+ multipart = dict(field)
1040
+ multipart["location"] = "multipart"
1041
+ out.append(multipart)
1042
+ return out
1043
+
1044
+
1045
+ def input_fields(j: dict, v1def: dict | None) -> list[dict]:
1046
+ """The op's input fields, taken from the set the COMPILER validates against.
1047
+
1048
+ There are two candidate schemas per op and they disagree for 47% of curated
1049
+ ops, so this choice is the whole point of the function:
1050
+
1051
+ * ``<op>.v1def.json`` → ``inputDefinition.fields`` — the Flow node's input
1052
+ contract. ``core/library.ts:106`` loads exactly this into ``fields``, and
1053
+ ``serialize.ts`` rejects anything outside it with `unknown input "…"`. The
1054
+ generated typed descriptors use it too.
1055
+ * ``<op>.json`` → ``inputSchema.fields`` — the connector's *API-level* schema
1056
+ from the registry. Richer, and it includes parameters the Flow node does not
1057
+ expose (pagination, ``where``/``orderBy``, upload ``file`` …).
1058
+
1059
+ File fields are the one exception: the Flow artifact has a multipart slot,
1060
+ and older catalog snapshots can carry the file only in ``inputSchema``.
1061
+ ``catalog_multipart_fields`` adds that transport field explicitly.
1062
+
1063
+ Documenting the second set makes the catalog advertise inputs the compiler
1064
+ refuses. Measured over the shipped library: of 1,337 curated ops, **585**
1065
+ listed at least one field that fails to compile, e.g. `pageSize` on
1066
+ `uipath-act-act365.list-all-activities` →
1067
+ `unknown input "pageSize"`. Another **49** hid a usable field the markdown
1068
+ never mentioned. Where the name sets already agree (703 ops) the required
1069
+ flags agree too, so those files are unchanged.
1070
+
1071
+ Falls back to ``inputSchema`` when an op ships no v1def, so nothing is lost
1072
+ for entries the Flow layer does not model.
1073
+ """
1074
+ fields = ((v1def or {}).get("inputDefinition") or {}).get("fields")
1075
+ if fields is None:
1076
+ fields = (j.get("inputSchema") or {}).get("fields") or []
1077
+ return fields
1078
+
1079
+
1080
+ def render_op(j: dict, v1def: dict | None = None, symbol: str | None = None,
1081
+ invariant: set | None = None) -> str:
1082
+ """Render one library entry to markdown.
1083
+
1084
+ Header layout (verb-first; both Curated and Generic share the same
1085
+ shape — only the values differ):
1086
+ # <nodeType>@<version>
1087
+ **Connector:** <key> (<name>)
1088
+ **Operation:** <verb> ← `op.name`, e.g. Create / Delete / Retrieve
1089
+ **Object:** <objectName> ← when present
1090
+ **Label:** <human-readable> ← `display.label`, when distinct
1091
+ **Description:** ...
1092
+ **HTTP:** <method> <pathTemplate-or-path>
1093
+ """
1094
+ invariant = invariant or set()
1095
+ node_type = j.get("nodeType", "?")
1096
+ version = j.get("version", "?")
1097
+ conn = j.get("connector", {}) or {}
1098
+ op = j.get("operation", {}) or {}
1099
+ display = j.get("display", {}) or {}
1100
+
1101
+ out: list[str] = []
1102
+ out.append(f"# {node_type}@{version}")
1103
+ out.append("")
1104
+
1105
+ conn_label = conn.get("key", "?")
1106
+ if conn.get("name"):
1107
+ conn_label += f" ({conn['name']})"
1108
+ out.append(f"**Connector:** {conn_label}")
1109
+
1110
+ verb = op.get("name") or ""
1111
+ if verb:
1112
+ out.append(f"**Operation:** {verb}")
1113
+
1114
+ object_name = op.get("objectName") or ""
1115
+ if object_name:
1116
+ out.append(f"**Object:** `{object_name}`")
1117
+
1118
+ label = (display.get("label") or "").strip()
1119
+ # Only show the human-readable label when it adds info beyond
1120
+ # `<verb> <object>` (which an agent can reconstruct).
1121
+ if label:
1122
+ out.append(f"**Label:** {label}")
1123
+
1124
+ desc = (display.get("description") or "").strip()
1125
+ if desc:
1126
+ out.append(f"**Description:** {desc}")
1127
+
1128
+ http_method = op.get("httpMethod") or ""
1129
+ http_path = op.get("pathTemplate") or op.get("path") or ""
1130
+ if http_method and http_path:
1131
+ out.append(f"**HTTP:** {http_method} {http_path}")
1132
+
1133
+ out.append("")
1134
+
1135
+ # Phase 1 marks every activity on a connector that advertises object/field
1136
+ # discovery. Some activities are fully static, while others gain fields or
1137
+ # objects from a live connection. Point the agent at `registry prepare`
1138
+ # when the static library does not contain what it needs, without claiming
1139
+ # every marked operation is necessarily incomplete.
1140
+ if (j.get("runtime") or {}).get("requiresConnectionForSchema"):
1141
+ key = conn.get("key") or "<key>"
1142
+ prefix = f"uipath.connector.{key}."
1143
+ action = node_type[len(prefix):] if node_type.startswith(prefix) else node_type.rsplit(".", 1)[-1]
1144
+ # A discovery entry is missing something different from the rest, and
1145
+ # `-f` cannot supply it. Of the 1,635 tier-1 pages carrying this block,
1146
+ # 30 are discovery entries: they have no inputs, and what has to be
1147
+ # resolved is the OBJECT. Pointing them at the parent-field loop and a
1148
+ # `-f <parent-field>=<value>` command describes a job that does not
1149
+ # apply, with a flag that cannot do it — `registry prepare` takes
1150
+ # `--object` for this.
1151
+ parents = ((j.get("operation") or {}).get("schemaAction") or {}).get(
1152
+ "parents"
1153
+ ) or []
1154
+ discovery = bool((j.get("operation") or {}).get("objectFromDiscovery"))
1155
+ if discovery:
1156
+ out.append(
1157
+ "> ⚠️ **The object is not in this catalog.** This node works on "
1158
+ "any object in the connection, including custom ones, so it "
1159
+ "carries no inputs here — what has to be resolved is the "
1160
+ "OBJECT, not a field value. Author first, then one command "
1161
+ "resolves it; **Choosing the object** below has the rules."
1162
+ )
1163
+ supply = "--object <name-as-the-task-said-it>"
1164
+ else:
1165
+ out.append(
1166
+ "> ⚠️ **This operation resolves its real inputs from a live "
1167
+ "connection.** The inputs listed below are only what the "
1168
+ "tenant-agnostic snapshot carries. Do NOT hand-guess the "
1169
+ "missing names."
1170
+ )
1171
+ out.append(">")
1172
+ if parents:
1173
+ # The section below names them, with the call that resolves
1174
+ # each — repeating the generic advice here just lengthens it.
1175
+ out.append(
1176
+ "> Its parents are named outright, with the call that "
1177
+ "resolves each one, under **Resolving the field set** "
1178
+ "below. Read that first."
1179
+ )
1180
+ else:
1181
+ # Kept even when the entry declares no inputs: they arrive from
1182
+ # the live describe, and the rule applies to them when they do.
1183
+ # `test_dynamic_schema_markdown_teaches_the_parent_field_loop`
1184
+ # holds this on every schema-dynamic op deliberately.
1185
+ out.append(
1186
+ "> Any input whose reference path carries a `{placeholder}` "
1187
+ "is a parent: resolve a value for EVERY parent first, then "
1188
+ "prepare the operation with all of them. Too few parents is "
1189
+ "refused; a wrong VALUE is not — it resolves right back to "
1190
+ "the parent fields."
1191
+ )
1192
+ # A bare prepare must never read as sufficient here: an op with no
1193
+ # declared inputs can still turn out to have parents once the live
1194
+ # schema arrives, and "2 fields came back" looks identical to
1195
+ # success. So the `-f` stays visible even when the catalog cannot
1196
+ # name the parents — only the prose above adapts.
1197
+ supply = (
1198
+ " ".join(f"-f {name}=<value>" for name in parents)
1199
+ if parents
1200
+ else "-f <parent-field>=<value> ..."
1201
+ )
1202
+ out.append(">")
1203
+ out.append("> ```bash")
1204
+ # ONE spelling on every page, and it is the one `check` prints. #660
1205
+ # measured the cost of the alternative: agents follow the documented
1206
+ # form, so a legacy `prepare-connector --connection-id <id>` here while
1207
+ # CUSTOM_FIELDS_UNPREPARED says `npx flow-sdk registry prepare` is two
1208
+ # routes on one page — and the agent takes the one it already knows.
1209
+ # No `--connection-id`: prepare discovers the connection itself and
1210
+ # writes bindings.json (folder key included).
1211
+ if discovery:
1212
+ # The name is matched, not equality-tested, so the task's casing
1213
+ # is fine as given.
1214
+ out.append(
1215
+ f"> npx flow-sdk registry prepare {key} {action} {supply}"
1216
+ )
1217
+ elif supply:
1218
+ out.append(f"> npx flow-sdk registry prepare {key} {action} \\")
1219
+ out.append(f"> {supply}")
1220
+ else:
1221
+ out.append(f"> npx flow-sdk registry prepare {key} {action}")
1222
+ out.append("> ```")
1223
+ out.append(">")
1224
+ overlay = (
1225
+ "> It writes a `./connectors-local/` overlay the compiler "
1226
+ "auto-detects (its fields are unioned onto this entry)."
1227
+ )
1228
+ if not discovery:
1229
+ # Pointing a discovery entry at the parent-field loop sends it to a
1230
+ # section about resolving values for parents it does not have.
1231
+ overlay += (
1232
+ " The full loop — how to find the parents and resolve their "
1233
+ "values — is in the connector guide "
1234
+ "(`references/connector-params.md`) under \"Schema-dynamic "
1235
+ "operations: the parent-field loop\"."
1236
+ )
1237
+ out.append(overlay)
1238
+ out.append(
1239
+ "> This requires a working live connection. For an explicitly "
1240
+ "offline/validate-only flow that needs no connection-specific fields, "
1241
+ "use the published static inputs below instead; a placeholder "
1242
+ "connection cannot materialize a live schema. If that input list is "
1243
+ "empty, pass `{}`: offline evidence proves operation identity and "
1244
+ "control routing, not the provider payload or dispatch."
1245
+ )
1246
+ out.append("")
1247
+
1248
+ # Body fields, catalog-only files, then declared parameters. Older snapshots
1249
+ # omitted a multipart parameter for some file uploads, while newer/live
1250
+ # entries declare it directly. A parameter remains authoritative on location,
1251
+ # so it REPLACES a same-named body or inferred multipart field.
1252
+ fields = [dict(f) for f in input_fields(j, v1def)]
1253
+ for p in [*catalog_multipart_fields(j), *param_fields(j)]:
1254
+ at = next((i for i, f in enumerate(fields) if f.get("name") == p["name"]), -1)
1255
+ if at >= 0:
1256
+ # Fill gaps rather than replace: the entry's field may already
1257
+ # carry a `reference` or `enum` this projection does not know
1258
+ # about, and overwriting it loses the lookup.
1259
+ merged = dict(fields[at])
1260
+ merged.update({k: v for k, v in p.items() if v not in (None, "")})
1261
+ for key in ("reference", "enum", "displayName"):
1262
+ if key in fields[at] and key not in p:
1263
+ merged[key] = fields[at][key]
1264
+ fields[at] = merged
1265
+ else:
1266
+ fields.append(p)
1267
+ required = [f for f in fields if f.get("required")]
1268
+ optional = [f for f in fields if not f.get("required")]
1269
+
1270
+ # Object selection comes before field selection, so it goes above the
1271
+ # inputs. For these operations the inputs below are whatever holds for
1272
+ # every object; the object's own fields arrive from step 2.
1273
+ if op.get("objectFromDiscovery"):
1274
+ connector_key = conn.get("key") or "<connector>"
1275
+ verb = op.get("name") or "<operation>"
1276
+ prefix = f"uipath.connector.{connector_key}."
1277
+ md_action = (
1278
+ node_type[len(prefix):] if node_type.startswith(prefix)
1279
+ else node_type.rsplit(".", 1)[-1]
1280
+ )
1281
+ out.append("## Choosing the object")
1282
+ out.append("")
1283
+ out.append(
1284
+ "This operation works on any object in the connection, including "
1285
+ "custom ones, so the object list is not in this catalog. Do not "
1286
+ "run discovery commands and do not hand-filter the catalog — "
1287
+ "author the step with the object name as the task said it (any "
1288
+ "casing), then one command matches it deterministically:"
1289
+ )
1290
+ out.append("")
1291
+ out.append("```bash")
1292
+ out.append(
1293
+ f"npx flow-sdk registry prepare {connector_key} {md_action} "
1294
+ "--object <name-as-the-task-said-it>"
1295
+ )
1296
+ out.append("```")
1297
+ out.append("")
1298
+ out.append(
1299
+ "The name is matched, never equality-tested: exact, then "
1300
+ "case-insensitive on API name and display name (`matched "
1301
+ "\"Company__C\" -> Company__c` is printed), narrowed to objects "
1302
+ f"that support {verb}. It then fetches the object's "
1303
+ "connection-resolved fields, generates the typed descriptor, and "
1304
+ "prints the import line to use."
1305
+ )
1306
+ out.append("")
1307
+ out.append(
1308
+ "**If the name matches several objects, the command refuses and "
1309
+ "prints the candidates** (API name, display name, custom, "
1310
+ "operations). `Custom: yes` marks an object that exists only on "
1311
+ "this tenant, so it could never have been in this catalog — a "
1312
+ "task that says \"custom\" means one of those rows. Pick by the "
1313
+ "task's wording, or present the candidates and let the user "
1314
+ "choose; never pick for them, and do not assume the user's "
1315
+ "wording is the API name. Then re-run with the chosen name."
1316
+ )
1317
+ out.append("")
1318
+ out.append(
1319
+ "**If nothing matches**, the command has already retried against "
1320
+ "a refreshed catalog before saying so — the object genuinely is "
1321
+ "not on this connection. Check the value against the task's "
1322
+ "words, or surface it to the user rather than inventing one."
1323
+ )
1324
+ out.append("")
1325
+
1326
+ action = op.get("schemaAction")
1327
+ if action:
1328
+ out.extend(schema_action_lines(
1329
+ conn.get("key") or "<connector>",
1330
+ op.get("objectName") or "",
1331
+ op.get("name") or "<operation>",
1332
+ action,
1333
+ fields,
1334
+ ))
1335
+
1336
+ out.append("## Required inputs")
1337
+ out.append("")
1338
+ if required:
1339
+ out.extend(field_line(f) for f in required)
1340
+ else:
1341
+ out.append("_(none)_")
1342
+ out.append("")
1343
+
1344
+ if optional:
1345
+ out.append("## Optional inputs")
1346
+ out.append("")
1347
+ out.extend(field_line(f) for f in optional)
1348
+ out.append("")
1349
+
1350
+ lookups = lookup_lines(
1351
+ conn.get("key") or "<connector>",
1352
+ node_type.rsplit(".", 1)[-1],
1353
+ fields, symbol, invariant,
1354
+ )
1355
+ if lookups:
1356
+ out.append("## Resolving lookups")
1357
+ out.append("")
1358
+ out.extend(lookups)
1359
+ out.append("")
1360
+
1361
+ # The example goes AFTER the lookups section on purpose: it uses the helpers
1362
+ # that section introduces, so a reader meets the rule before the usage.
1363
+ out.extend(example_lines(j, fields, symbol, invariant, v1def))
1364
+
1365
+ out.extend(event_filter_lines(j, v1def))
1366
+
1367
+ out_fields = (j.get("outputSchema") or {}).get("fields") or []
1368
+ if out_fields:
1369
+ out.append("## Outputs")
1370
+ out.append("")
1371
+ out.extend(field_line(f) for f in out_fields)
1372
+ out.append("")
1373
+
1374
+ return "\n".join(out)
1375
+
1376
+
1377
+ def parse_args() -> argparse.Namespace:
1378
+ p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
1379
+ p.add_argument("--source", required=True,
1380
+ help="Source JSON library dir (must contain index.json)")
1381
+ p.add_argument("--output", required=True,
1382
+ help="Destination dir for the markdown library")
1383
+ return p.parse_args()
1384
+
1385
+
1386
+ def main() -> int:
1387
+ args = parse_args()
1388
+ src = Path(args.source).resolve()
1389
+ dst = Path(args.output).resolve()
1390
+
1391
+ index_path = src / "index.json"
1392
+ if not index_path.is_file():
1393
+ print(f"source library missing index.json: {index_path}", file=sys.stderr)
1394
+ return 1
1395
+
1396
+ index = json.loads(index_path.read_text())
1397
+ entries = index.get("entries", []) or []
1398
+ # The SAME map the descriptor generator uses, so a symbol cited here is one
1399
+ # it actually emitted. An entry absent from the map has no descriptor and
1400
+ # its example is authored through the library instead (see symbol_index).
1401
+ symbols = symbol_index(entries)
1402
+ invariant = load_invariant_collections(src)
1403
+
1404
+ # Wipe and recreate destination so stale entries don't linger.
1405
+ if dst.exists():
1406
+ shutil.rmtree(dst)
1407
+ dst.mkdir(parents=True, exist_ok=True)
1408
+
1409
+ converted = 0
1410
+ for entry in entries:
1411
+ rel_json = entry.get("path") or ""
1412
+ if not rel_json or not rel_json.endswith(".json"):
1413
+ continue
1414
+ src_path = src / rel_json
1415
+ if not src_path.is_file():
1416
+ print(f" WARN: missing source entry {src_path}", file=sys.stderr)
1417
+ continue
1418
+ j = json.loads(src_path.read_text())
1419
+ # The Flow node shape lives beside the entry as `<op>.v1def.json`; its
1420
+ # inputDefinition is what the compiler validates, so inputs are rendered
1421
+ # from it (see input_fields). Outputs stay on the entry's outputSchema —
1422
+ # v1def carries no output fields, and the descriptor generator agrees.
1423
+ v1_path = src_path.with_name(src_path.name[:-5] + ".v1def.json")
1424
+ v1def = json.loads(v1_path.read_text()) if v1_path.is_file() else None
1425
+ md = render_op(j, v1def, symbols.get(entry.get("nodeType")), invariant)
1426
+ rel_md = rel_json[:-5] + ".md"
1427
+ out_path = dst / rel_md
1428
+ out_path.parent.mkdir(parents=True, exist_ok=True)
1429
+ out_path.write_text(md)
1430
+ # Mutate the entry in place so the rewritten index points at .md.
1431
+ entry["path"] = rel_md
1432
+ converted += 1
1433
+
1434
+ (dst / "index.json").write_text(json.dumps(index, indent=2) + "\n")
1435
+
1436
+ print(f"wrote {converted} markdown ops to {dst}")
1437
+ return 0
1438
+
1439
+
1440
+ if __name__ == "__main__":
1441
+ sys.exit(main())