@xano/sdk 1.0.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 (336) hide show
  1. package/CHANGELOG.md +788 -0
  2. package/LICENSE +21 -0
  3. package/README.md +541 -0
  4. package/dist/.build-fingerprint +1 -0
  5. package/dist/agent-file-refresh-ZEMK33AX.js +38 -0
  6. package/dist/answer-shape-K67TAAPY.js +33 -0
  7. package/dist/backend-dir-2U53CIAL.js +39 -0
  8. package/dist/backend-slot-7QXW2QYC.js +28 -0
  9. package/dist/bin.d.ts +1 -0
  10. package/dist/bin.js +255 -0
  11. package/dist/branch-NCRNWQBQ.js +61 -0
  12. package/dist/branch-commands-ACIRCEQJ.js +79 -0
  13. package/dist/bundle.d.ts +223 -0
  14. package/dist/bundle.js +153 -0
  15. package/dist/capture-WAXPD7DR.js +41 -0
  16. package/dist/chunk-27GWNP2I.js +23 -0
  17. package/dist/chunk-2CMJZDCL.js +177 -0
  18. package/dist/chunk-2EZ3ZJZZ.js +302 -0
  19. package/dist/chunk-2GNFDZA2.js +390 -0
  20. package/dist/chunk-2MNLRIOX.js +11 -0
  21. package/dist/chunk-2TFRR6JV.js +104 -0
  22. package/dist/chunk-2ZUEE4EK.js +408 -0
  23. package/dist/chunk-36JUQZVQ.js +22 -0
  24. package/dist/chunk-3DRXWLJ5.js +83 -0
  25. package/dist/chunk-3JDU4VRL.js +137 -0
  26. package/dist/chunk-3KZCLN4Z.js +241 -0
  27. package/dist/chunk-3LDULPRE.js +168 -0
  28. package/dist/chunk-3M4LFYNQ.js +111 -0
  29. package/dist/chunk-3XI5CTUE.js +22 -0
  30. package/dist/chunk-3YW23UFH.js +50 -0
  31. package/dist/chunk-45UIT5T2.js +862 -0
  32. package/dist/chunk-4BW2SG2N.js +46 -0
  33. package/dist/chunk-4KHEMC32.js +79 -0
  34. package/dist/chunk-4POWHT2K.js +321 -0
  35. package/dist/chunk-4Q2GN7NW.js +192 -0
  36. package/dist/chunk-5A2PLKZB.js +70 -0
  37. package/dist/chunk-5FBYXK5P.js +159 -0
  38. package/dist/chunk-5HVXPTU4.js +323 -0
  39. package/dist/chunk-5QU2P5CA.js +304 -0
  40. package/dist/chunk-5V5XU32D.js +191 -0
  41. package/dist/chunk-5ZSO4T2G.js +10 -0
  42. package/dist/chunk-622HUCZT.js +10 -0
  43. package/dist/chunk-65HWOMGV.js +177 -0
  44. package/dist/chunk-6ELFRN3P.js +365 -0
  45. package/dist/chunk-6HO3NCSJ.js +1055 -0
  46. package/dist/chunk-6IUMKYWI.js +457 -0
  47. package/dist/chunk-6KEB3C3U.js +65 -0
  48. package/dist/chunk-6MR5TI5V.js +987 -0
  49. package/dist/chunk-6NNTNXN7.js +157 -0
  50. package/dist/chunk-6PBR37LN.js +97 -0
  51. package/dist/chunk-6QXLMMT3.js +155 -0
  52. package/dist/chunk-6ZGJCBZS.js +224 -0
  53. package/dist/chunk-73IVBKMA.js +250 -0
  54. package/dist/chunk-7G3CTU7I.js +286 -0
  55. package/dist/chunk-A3UDOZB6.js +222 -0
  56. package/dist/chunk-A6E77GDW.js +85 -0
  57. package/dist/chunk-AHJWCYQO.js +660 -0
  58. package/dist/chunk-AKNCFVKZ.js +36 -0
  59. package/dist/chunk-AM4FKL4J.js +237 -0
  60. package/dist/chunk-AN6BL47T.js +26 -0
  61. package/dist/chunk-AOSYVMUS.js +147 -0
  62. package/dist/chunk-APWXY5SL.js +182 -0
  63. package/dist/chunk-AWEIIR2U.js +218 -0
  64. package/dist/chunk-AWQ7BQTQ.js +73 -0
  65. package/dist/chunk-BAUZOWGX.js +393 -0
  66. package/dist/chunk-BHACRS6R.js +940 -0
  67. package/dist/chunk-BWPNT55T.js +680 -0
  68. package/dist/chunk-C6RIMRU4.js +187 -0
  69. package/dist/chunk-CGVQZXYG.js +263 -0
  70. package/dist/chunk-CIMQZRRE.js +162 -0
  71. package/dist/chunk-CJHQKJU4.js +113 -0
  72. package/dist/chunk-CTMO46PK.js +1897 -0
  73. package/dist/chunk-D53J55C5.js +106 -0
  74. package/dist/chunk-D7VGG3WS.js +90 -0
  75. package/dist/chunk-DAD3ONIS.js +245 -0
  76. package/dist/chunk-DGWHAVRG.js +162 -0
  77. package/dist/chunk-DX4QR4B2.js +483 -0
  78. package/dist/chunk-EHP3WPEG.js +21 -0
  79. package/dist/chunk-ELP2HMXC.js +432 -0
  80. package/dist/chunk-ERVIASGL.js +21 -0
  81. package/dist/chunk-ET4Y4XCB.js +865 -0
  82. package/dist/chunk-ETO6LGLD.js +4750 -0
  83. package/dist/chunk-EUUUMY7J.js +24 -0
  84. package/dist/chunk-FQJ2TTHP.js +218 -0
  85. package/dist/chunk-FQOF7E2E.js +133 -0
  86. package/dist/chunk-FWD7AL6N.js +21 -0
  87. package/dist/chunk-FXFV3KU6.js +138 -0
  88. package/dist/chunk-FXRQOWTE.js +171 -0
  89. package/dist/chunk-G554NZNI.js +210 -0
  90. package/dist/chunk-GSGDWJB5.js +13 -0
  91. package/dist/chunk-H2V3N23D.js +66 -0
  92. package/dist/chunk-H5C7MBCR.js +159 -0
  93. package/dist/chunk-HHN734BU.js +371 -0
  94. package/dist/chunk-HRVBLCVJ.js +243 -0
  95. package/dist/chunk-HVRUQXGD.js +1264 -0
  96. package/dist/chunk-I4F5TCJH.js +130 -0
  97. package/dist/chunk-IDOTCMDV.js +57 -0
  98. package/dist/chunk-IGOLHKWF.js +427 -0
  99. package/dist/chunk-JGE7CAWB.js +148 -0
  100. package/dist/chunk-JTUKLCXS.js +92 -0
  101. package/dist/chunk-JZGVVJSI.js +66 -0
  102. package/dist/chunk-K4WX6UUU.js +923 -0
  103. package/dist/chunk-K5BBGTYZ.js +217 -0
  104. package/dist/chunk-K6RXBGUB.js +427 -0
  105. package/dist/chunk-KTSDSD36.js +165 -0
  106. package/dist/chunk-L5JYN3NM.js +1038 -0
  107. package/dist/chunk-LIEOKXKD.js +354 -0
  108. package/dist/chunk-LLNZGEFK.js +51 -0
  109. package/dist/chunk-LPECSJ2J.js +55 -0
  110. package/dist/chunk-LQCMKAJ7.js +413 -0
  111. package/dist/chunk-LRN4ZJCQ.js +1145 -0
  112. package/dist/chunk-MJ4ZVHKN.js +17 -0
  113. package/dist/chunk-MXHSQOC4.js +3061 -0
  114. package/dist/chunk-N5GKCWE5.js +367 -0
  115. package/dist/chunk-NA3PXMEF.js +10 -0
  116. package/dist/chunk-NAQDMEHV.js +76 -0
  117. package/dist/chunk-NWKXM5RD.js +109 -0
  118. package/dist/chunk-NZOWWHU5.js +196 -0
  119. package/dist/chunk-OHX6MIUZ.js +184 -0
  120. package/dist/chunk-OOXWYAA4.js +15 -0
  121. package/dist/chunk-OPHB5ORL.js +1990 -0
  122. package/dist/chunk-OXA6PSZT.js +1725 -0
  123. package/dist/chunk-P2HAGF6H.js +73 -0
  124. package/dist/chunk-PTHVMXZI.js +3036 -0
  125. package/dist/chunk-PZ5AY32C.js +10 -0
  126. package/dist/chunk-Q325JKYB.js +189 -0
  127. package/dist/chunk-Q7DGTQ5G.js +255 -0
  128. package/dist/chunk-QCNYHBL7.js +601 -0
  129. package/dist/chunk-QO6BVGS5.js +145 -0
  130. package/dist/chunk-QXTKSOFI.js +76 -0
  131. package/dist/chunk-ROY4YAEQ.js +66 -0
  132. package/dist/chunk-RTJRZFFY.js +447 -0
  133. package/dist/chunk-SAVJUS2X.js +128 -0
  134. package/dist/chunk-SFLHVCKX.js +173 -0
  135. package/dist/chunk-STSFNCY4.js +127 -0
  136. package/dist/chunk-SWBF2LTK.js +170 -0
  137. package/dist/chunk-T5KELPAR.js +109 -0
  138. package/dist/chunk-TFFGOFVZ.js +1481 -0
  139. package/dist/chunk-TLHIFK4M.js +256 -0
  140. package/dist/chunk-TLQE7ZN3.js +156 -0
  141. package/dist/chunk-TPHWJ7UG.js +1981 -0
  142. package/dist/chunk-TRJCALW3.js +291 -0
  143. package/dist/chunk-TTEIYD5P.js +133 -0
  144. package/dist/chunk-U64O2MXY.js +407 -0
  145. package/dist/chunk-UCQ2VUTG.js +630 -0
  146. package/dist/chunk-UTI5G6OJ.js +44 -0
  147. package/dist/chunk-UVHDMMIP.js +188 -0
  148. package/dist/chunk-UZ6QQWJT.js +120 -0
  149. package/dist/chunk-VAX3UZOY.js +686 -0
  150. package/dist/chunk-W3HBUKFO.js +57 -0
  151. package/dist/chunk-WKQLGKCL.js +2823 -0
  152. package/dist/chunk-WQFNASK4.js +560 -0
  153. package/dist/chunk-WRBKLYSJ.js +224 -0
  154. package/dist/chunk-WTPZRR4I.js +17 -0
  155. package/dist/chunk-WZDJ2LLX.js +2385 -0
  156. package/dist/chunk-X2NL4KUO.js +14 -0
  157. package/dist/chunk-X4DVXBFY.js +38 -0
  158. package/dist/chunk-X677DMZM.js +7050 -0
  159. package/dist/chunk-XBKMWY7F.js +612 -0
  160. package/dist/chunk-XNCP4KYO.js +78 -0
  161. package/dist/chunk-XVBGLISP.js +35 -0
  162. package/dist/chunk-XW6KISBB.js +1192 -0
  163. package/dist/chunk-YGZHHAAB.js +8156 -0
  164. package/dist/chunk-YHEFQUQB.js +186 -0
  165. package/dist/chunk-YLDALTWZ.js +653 -0
  166. package/dist/chunk-YU4HSQW7.js +229 -0
  167. package/dist/chunk-ZDPP6RKB.js +27 -0
  168. package/dist/chunk-ZFDZKNGG.js +194 -0
  169. package/dist/chunk-ZIY7RNUT.js +49 -0
  170. package/dist/chunk-ZYJP6BOO.js +30 -0
  171. package/dist/cli.d.ts +1111 -0
  172. package/dist/cli.js +165 -0
  173. package/dist/codegen-command-VR2GWR6C.js +173 -0
  174. package/dist/codegen.d.ts +115 -0
  175. package/dist/codegen.js +19 -0
  176. package/dist/commands-GJMJ4XYZ.js +76 -0
  177. package/dist/completion-ZQLO6PB6.js +738 -0
  178. package/dist/config-MBYGCAFK.js +136 -0
  179. package/dist/deploy-command-JVZPWHMK.js +180 -0
  180. package/dist/deploy-source-3W45CXMP.js +81 -0
  181. package/dist/dev-env-sync-HN44K4D5.js +188 -0
  182. package/dist/diagnostics-UEAAHWVF.js +30 -0
  183. package/dist/display-name-hint-ABBPVQIR.js +46 -0
  184. package/dist/emit-CyhBDxli.d.ts +7007 -0
  185. package/dist/env-example-refresh-ET5HMMDW.js +126 -0
  186. package/dist/env-pull-command-TNMNB3CV.js +323 -0
  187. package/dist/env-target-O4HRKTGZ.js +40 -0
  188. package/dist/env-var-command-ZIRDCAZA.js +427 -0
  189. package/dist/ephemeral-P7F3PMAD.js +54 -0
  190. package/dist/ephemeral-command-CLJJKOFT.js +592 -0
  191. package/dist/ephemeral-state-PYCBSOGX.js +61 -0
  192. package/dist/file-lock-PT74GWYW.js +10 -0
  193. package/dist/generate-command-AYEOLA26.js +510 -0
  194. package/dist/gitignore-3SJ3WNRG.js +51 -0
  195. package/dist/guards-4EBNCZDC.js +162 -0
  196. package/dist/hosted-file-PSHJB3HX.js +28 -0
  197. package/dist/impersonate-command-5JAS3WSV.js +152 -0
  198. package/dist/import-EQSFU6JC.js +19 -0
  199. package/dist/index.d.ts +5470 -0
  200. package/dist/index.js +371 -0
  201. package/dist/init-command-X2FR4UAK.js +125 -0
  202. package/dist/init-modules-RY7UHAOL.js +50 -0
  203. package/dist/init-web-M4YZSZK6.js +42 -0
  204. package/dist/internal.d.ts +179 -0
  205. package/dist/internal.js +2634 -0
  206. package/dist/io-RC56QSN5.js +26 -0
  207. package/dist/keep-data-merge-7MPLVAVI.js +1119 -0
  208. package/dist/lambda-config-reconcile-OUZ4OYAB.js +116 -0
  209. package/dist/landed-753YLJ5H.js +40 -0
  210. package/dist/landing-record-SU52SKXU.js +61 -0
  211. package/dist/live-diff-YONW2VI3.js +65 -0
  212. package/dist/local-engine-choice-CH5X3W7E.js +79 -0
  213. package/dist/local-engine-command-Z3OL7VOD.js +766 -0
  214. package/dist/local-engine-config-5U3SC434.js +39 -0
  215. package/dist/local-engine-deploy-PCQ4SNPB.js +39 -0
  216. package/dist/local-engine-process-EHTE5LLU.js +63 -0
  217. package/dist/local-engine-state-A4FTRFAF.js +32 -0
  218. package/dist/local-engine-update-check-6L645VBL.js +162 -0
  219. package/dist/lock-MRRHW5GS.js +87 -0
  220. package/dist/lock-commands-2QG25HL7.js +963 -0
  221. package/dist/login-command-QPYRWOXZ.js +538 -0
  222. package/dist/logout-command-WOIQLDP6.js +56 -0
  223. package/dist/loop-ALIZLOSX.js +191 -0
  224. package/dist/marketplace-command-K5YL42NO.js +977 -0
  225. package/dist/marketplace-resolve-LY32JES5.js +40 -0
  226. package/dist/meta-client-YXGPW53C.js +244 -0
  227. package/dist/module-manifest-JFGHU2TC.js +38 -0
  228. package/dist/near-key.d.ts +13 -0
  229. package/dist/near-key.js +8 -0
  230. package/dist/node.d.ts +95 -0
  231. package/dist/node.js +969 -0
  232. package/dist/operation-outcome-TZSESNC7.js +51 -0
  233. package/dist/output-target-AL3I44GG.js +47 -0
  234. package/dist/package-manager-PRAHUSCN.js +34 -0
  235. package/dist/plan-disclosure-6G4MLZKD.js +114 -0
  236. package/dist/plugin-contributions-WRNY5ABG.js +28 -0
  237. package/dist/plugin-prompt-4VA3B4FF.js +20 -0
  238. package/dist/plugin-questions-JTSYA74Q.js +24 -0
  239. package/dist/plugin.d.ts +396 -0
  240. package/dist/plugin.js +1 -0
  241. package/dist/preflight-command-2T5Z36MI.js +321 -0
  242. package/dist/profile-command-AH3PIM4X.js +800 -0
  243. package/dist/profile-pointer-GRWJU3YD.js +31 -0
  244. package/dist/profile-select-74TRDEPD.js +23 -0
  245. package/dist/project-dependency-KSH63ODT.js +35 -0
  246. package/dist/project-sdk-REPPOIBD.js +24 -0
  247. package/dist/promote-command-PCWT5N6S.js +140 -0
  248. package/dist/prompt-II3F3LND.js +27 -0
  249. package/dist/publish-command-TTDUUJI5.js +414 -0
  250. package/dist/pull-command-VBO7C4S3.js +161 -0
  251. package/dist/pull-superseded-GJWLG7XO.js +20 -0
  252. package/dist/release-command-N4ZPKMHR.js +146 -0
  253. package/dist/release-ns-command-3KFDMZ6G.js +1745 -0
  254. package/dist/response-HlXahv4K.d.ts +925 -0
  255. package/dist/retry-command-AS5EVREX.js +34 -0
  256. package/dist/routes-manifest-SNWUVBT6.js +43 -0
  257. package/dist/runtime-XETHVF33.js +14 -0
  258. package/dist/scaffold.d.ts +1911 -0
  259. package/dist/scaffold.js +107 -0
  260. package/dist/secrets-file-LVZD6IIM.js +56 -0
  261. package/dist/secrets-fill-command-TTCPSVXN.js +218 -0
  262. package/dist/seed-VU7C3QYS.js +46 -0
  263. package/dist/sent-writes-N7X4OVXT.js +22 -0
  264. package/dist/source-resolve-S2XA3XZF.js +83 -0
  265. package/dist/static-host-B2Y6AG3W.js +352 -0
  266. package/dist/status-command-RUDXTF7I.js +554 -0
  267. package/dist/store-6VNCD4D7.js +20 -0
  268. package/dist/store-ARM6VMO5.js +98 -0
  269. package/dist/store-DuYfzKlW.d.ts +3144 -0
  270. package/dist/suggest-6FOOYKHG.js +12 -0
  271. package/dist/sveltekit.d.ts +154 -0
  272. package/dist/sveltekit.js +109 -0
  273. package/dist/sync-contributions-UCEROO26.js +415 -0
  274. package/dist/table-ENAVMITR.js +42 -0
  275. package/dist/tables-command-GUPXXSB5.js +156 -0
  276. package/dist/tenant-IIHPOHGP.js +42 -0
  277. package/dist/tenant-command-D5HGI2X4.js +965 -0
  278. package/dist/test-command-TIKZMDPZ.js +476 -0
  279. package/dist/tests-BGGIOK3L.js +26 -0
  280. package/dist/token-NICJUTMT.js +72 -0
  281. package/dist/toolchain-hooks-GHKLUJOH.js +96 -0
  282. package/dist/toolchain-modules-5EAZUONB.js +24 -0
  283. package/dist/tracked-backend-EIBUMDZA.js +80 -0
  284. package/dist/update-check-2W2NLDTC.js +136 -0
  285. package/dist/upgrade-command-QHNFWM7O.js +531 -0
  286. package/dist/verify-rollout-DRCK5TDD.js +61 -0
  287. package/dist/whoami-command-STGINTXH.js +51 -0
  288. package/dist/workspace-2CLHU2LK.js +93 -0
  289. package/dist/workspace-binding-56PIIPCP.js +54 -0
  290. package/dist/workspace-command-JEXQETJD.js +535 -0
  291. package/dist/workspace-env-F2RFMSC5.js +70 -0
  292. package/dist/workspace-export-OJSH2HIW.js +21 -0
  293. package/dist/writable-VA7OISSK.js +25 -0
  294. package/dist/xano-ICFUSPFR.js +42 -0
  295. package/dist/xanosdk-import-GN42TO2B.js +56 -0
  296. package/dist/xanosdk-project-S6DXJONQ.js +41 -0
  297. package/dist/xdo-CxDr2Q1C.d.ts +1371 -0
  298. package/guides/README.md +33 -0
  299. package/guides/authoring.md +921 -0
  300. package/guides/cli.md +401 -0
  301. package/guides/codegen.md +177 -0
  302. package/guides/codes.md +404 -0
  303. package/guides/coverage.md +74 -0
  304. package/guides/deploying.md +1525 -0
  305. package/guides/environment.md +400 -0
  306. package/guides/marketplace.md +415 -0
  307. package/guides/module-contract.md +558 -0
  308. package/guides/object-kinds.md +433 -0
  309. package/guides/project-structure.md +43 -0
  310. package/guides/scaffold.md +351 -0
  311. package/guides/typed-frontend.md +242 -0
  312. package/llms/client.md +63 -0
  313. package/llms/errors.md +56 -0
  314. package/llms/fields.md +176 -0
  315. package/llms/filters.md +183 -0
  316. package/llms/kinds-agent-mcp.md +30 -0
  317. package/llms/kinds-core.md +100 -0
  318. package/llms/kinds-knowledge.md +20 -0
  319. package/llms/kinds-realtime.md +76 -0
  320. package/llms/lambda.md +94 -0
  321. package/llms/legacy.md +44 -0
  322. package/llms/lock.md +40 -0
  323. package/llms/object-kinds.md +34 -0
  324. package/llms/statements-calls.md +62 -0
  325. package/llms/statements-catalog.md +320 -0
  326. package/llms/statements-data.md +66 -0
  327. package/llms/statements-runtime.md +68 -0
  328. package/llms/tests.md +59 -0
  329. package/llms/toolchain.md +16 -0
  330. package/llms/triggers.md +36 -0
  331. package/llms/values.md +29 -0
  332. package/llms-full.txt +2085 -0
  333. package/llms.txt +493 -0
  334. package/manifest.json +11224 -0
  335. package/package.json +162 -0
  336. package/types/lambda-globals.d.ts +129 -0
package/llms/errors.md ADDED
@@ -0,0 +1,56 @@
1
+ # Error index
2
+
3
+ > Read when a request, test, build, or pull fails with a message you did not write — the string, its cause, and where the fix is.
4
+
5
+ ### Request-time (deployed) failures
6
+
7
+ - `Unable to locate input: <name>` — `inp()` names an undeclared input, or an attached middleware reads its own `input` (never bound). Declare it; a stack value is `ref("var.field")`; middleware reads the body with `s.util.get_all_input`. `llms.txt`; `llms/kinds-core.md`.
8
+ - `Unable to locate var: <a.b>` (HTTP 500) — a dotted `ref` into a null base (a `db.get` miss), or a column absent from the row (`f.password` is `internal`). Guard existence first, or use the null-safe `safe` option; name the column in `output`. In a `post` middleware, `<var>.vars` from `s.util.get_all_input` holds `{ status, result }` (plus `payload` on an error), not the request. `llms.txt`; `llms/kinds-core.md`.
9
+ - `Missing var entry: <name>` inside `s.expect.to_throw` — the body runs in an isolated var stack; bind what it needs inside the body. `llms/tests.md`.
10
+ - `Missing param: field_value` (HTTP 400) — a `db.*` match argument resolved to `null` (a safe ref, a nullable foreign key). Guard existence before the lookup; an optional FK stores `0` and is read with field-match `db.get`. `llms.txt`; `llms/fields.md`.
11
+ - `Value is less than the minimum value of 1` (HTTP 400) — a `db.get` given `0` in an editor-authored workspace (SDK field-match `db.get` answers 200 + null). `llms/statements-data.md`.
12
+ - `Invalid op: <op>` — a `cmp` operator (`in`, `like`, …) in a RUNTIME condition, which takes the `expr` set only; write `or(expr(...), expr(...))`. `llms/kinds-core.md`.
13
+ - `Unsupported param format` / `Unsupported parameter reference` — an aggregate or `eval` `name` that is not a bare column, or a bare `eval` name for a joined column. `llms/statements-data.md`.
14
+ - `ParseError: Invalid value for param` (HTTP 400) — `contains`/`@>`/`overlaps` on a text column (use `includes`), or a joined column qualified without `tableAlias`. `llms/statements-data.md`.
15
+ - `ERROR_FATAL: <Type> does not exist: <type>:<n>` — `s.api.call`/`s.task.call`/`s.trigger.call`/`s.workflow_test.call` outside a workflow test; share logic via `defineFunction` + `s.function.run`. `llms.txt`.
16
+ - `ERROR_FATAL "Unable to decode."` — a populated JSON string where the engine expects `c.obj`'s form; write `c.obj({...})` or `obj({...})`. `llms/values.md`.
17
+ - `Precondition failed.` in place of your message — `error` was a bare string; pass `c.text("…")`. `llms/fields.md`.
18
+ - `Param: token - Text filter requires an integer, float, string or boolean value` — `s.api.call` `auth.token` given a tagged `Value`; it must be a bare string. `llms/statements-calls.md`.
19
+ - Every `s.db.add` into one table 400s naming a column while complaining about its VALUE — the column is named `run`. `llms.txt`.
20
+ - HTTP 200 with an EMPTY body where `0` was expected — a bare `returnType: "count"` of zero; wrap it as `{ count: ref("n") }`. `llms/statements-data.md`.
21
+ - HTTP 200 with `[]` from a `db.query` that matches rows — a paged query (`metadata: true`, the default) whose `output` names bare columns; the selection applies to the ENVELOPE, so prefix them (`items.<column>`) or set `metadata: false` inside `paging`. `llms.txt`; `llms/statements-data.md`.
22
+ - Every field mapped off an `s.api.request` result reads `null` — the upstream answered an error (a 403 for a missing `User-Agent` is the common one) and its body arrived as ordinary `response.result`; send a `User-Agent` and gate on `response.status`. `llms/statements-calls.md`.
23
+ - A lambda returns error TEXT with HTTP 200 (`Could not resolve "node:crypto"`, an undefined-binding message) — the body threw; use the preloaded globals, the surface's bindings, and `capture`. `llms/lambda.md`.
24
+ - `Unable to locate request.` (404) on an endpoint that deployed — a lowercase verb, a `.` in the name, or a CORS `mode` typo that dropped the whole API group. `llms/kinds-core.md`.
25
+ - A browser call fails on a missing `access-control-allow-origin` while deploy looks fine — CORS `mode: "custom"` with `"*"` or an unmatched origin. `llms/kinds-core.md`.
26
+ - A websocket is closed right after the handshake — a refused `connect` gate: an empty/falsy return, a crash, or a gating trigger with no `response`. `llms/triggers.md`.
27
+ - `to_throw failed - response is ok` around an auth-refused call — `ERROR_CODE_ACCESS_DENIED` carries no message; the auth gate is not reachable from a workflow test. `llms/tests.md`.
28
+ - Output reads `Hi [object Object]` — a tagged value inside a JS template literal; compose at runtime with `withFilters` + `fl.concat`. `llms.txt`.
29
+
30
+ ### Build-time and tooling failures
31
+
32
+ - `export --strict` fails on a warning whose shape is meant (a fixture pinning a hazard) — accept it on ITS def only with `diagnostics: { allow: ["<code>"] }`, which takes any warning code that def raised (never an error) on every def kind. Accepted warnings are still listed: `export({ accepted: [] })`, `--json` `accepted[]`. An allowed code the def no longer raises warns `diagnostics.allow-unused`.
33
+ - `must be ES modules` — package.json says `"type": "commonjs"`; set `"type": "module"`. `llms.txt` Quickstart.
34
+ - A response types as `StackTupleWidened` — a `Statement[]` helper was spread into the stack; return `statements(...)`. `llms.txt`; `llms/statements-runtime.md`.
35
+ - `ERR_PACKAGE_PATH_NOT_EXPORTED` — `tsx -e "import …"`, or running from outside the project root; run a real file with `tsx <file.ts>` from the root. `llms/client.md`.
36
+ - `Missing statement: mvp:placeholder` on import — an unconfigured slot in a pulled tree; replace it with the statement it stands in for. `llms/legacy.md`.
37
+ - A CLI command fails under `--json` or piped (unless stdout carries data): stdout is `{ ok: false, error: { code, message, exitCode, details?, suggestion? } }`, as does a failed release write. Codes: `SDK_SEED_IN_STATIC` (`details.leaks[]` → `publicSeed`), `SDK_EXPORT_INVALID` (`details.diagnostics[]`, `--strict` too), `SDK_DRIFT`, `SDK_BRANCH_TAKEN`, `SDK_BRANCH_LIVE`, `SDK_RELEASE_NAME_TAKEN`, `SDK_RELEASE_FILES_NOT_CARRIED`, `SDK_LIVE_BRANCH_MISMATCH` (`details.conflictsWith`), `SDK_IDENTITY_CONFLICT`, `SDK_KIND_CONFLICT`, `SDK_PRUNE_OUT_OF_SCOPE`, `SDK_IMPORT_REFUSED`, `SDK_SHARED_SCHEMA_CHANGE`, `SDK_CONSTRAINT_VIOLATION`, `SDK_CREDENTIAL_REJECTED` (`details`: `{ profile, credentialType, instance, workspaceId, signIn }`; ids null on a refused env refresh), `SDK_USAGE` (`details.reason: "needs-confirmation"`: run its `Re-run as`), `SDK_ERROR`. Landing refusal `details.refused`: identityConflict, renamePending, kindConflict, uniqueViolation, tableTrigger, importInProgress, importRefused, pruneOutOfScope. `warnings[]` / `details.warnings` / `accepted[]` entries: `{ code, message, subject? }` (`subject`: the def's `{ kind, name }`). Backend documents add `selector` and `workspaceId`.
38
+ - A CLI command reports `could not reach <url>: fetch failed (<code>)` — blocked egress, not a bad credential; fix it before `xanosdk login`, which rotates a single-use refresh token.
39
+
40
+ ### CLI exit codes
41
+
42
+ A failure document's `exitCode` matches.
43
+
44
+ | Code | Meaning | Next step |
45
+ |---|---|---|
46
+ | 0 | Success, and a `delete` of a missing name (`alreadyGone`). | — |
47
+ | 1 | A usage error (a missing local file included), a refused request, or anything not listed. | Read the message. |
48
+ | 2 | Ran and disagreed: `preflight` failed, `workspace diff` differed, `promote`, a release or an import did not land as sent or hit a conflict, the `init --from`/`pull` round-trip check failed, a lock or branch-label conflict, or `init` could not install its dependencies or an add-on (scaffolded anyway). | Fix what it reports. |
49
+ | 3 | `deploy --static`: the backend landed, the static site did not. | Deploy the site again. |
50
+ | 4 | `deploy --require-microservices`: a microservice was not ready in time. The backend landed. | Check the microservice. |
51
+ | 5 | `test` / `deploy --test`: the suite ran and a test failed. A deploy is not retracted. | Fix the test or the code. |
52
+ | 6 | `test`, `deploy --test`: the suite could not be reached. | Check the backend; rerun. |
53
+ | 7 | `upgrade --check`: an upgrade is available. | `xanosdk upgrade`. |
54
+ | 8 | A named backend could not be addressed (or a named release, profile, branch, test, module, lock entry, `--seed` guid or local-engine version is missing): gone, expired, unreachable, a stopped local engine, or a busy import. | Retry, or redeploy. |
55
+ | 9 | A write whose outcome is unknown, Ctrl-C during a write included. | Read the target's state before retrying. |
56
+ | 130 | Ctrl-C outside a write or install. | — |
package/llms/fields.md ADDED
@@ -0,0 +1,176 @@
1
+ # Column and input types
2
+
3
+ > Read when declaring a table column (`f.*`) or a function/query input (`input.*`) — a type's options and methods, and the `s.precondition` error/status contract. Also `table({ seed })` rows and `use_xdo`.
4
+
5
+ Author table columns + function/API inputs with the typed catalog: `f.<type>(opts?)`
6
+ for columns, `input.<type>(opts?)` for inputs. Common opts: `required`, `nullable`,
7
+ `default`, `description`, `access` (a read omits `"internal"` columns, returns `"private"` ones; `InferRow`
8
+ is the full row, both kept), `sensitive` (masks the input's value in request logs; a read still RETURNS a sensitive column — `access: "internal"` keeps it out).
9
+ **`nullable` defaults PER TYPE, matching the engine's own column-creation API**: `true`
10
+ for `f.vector`, `f.uuid`, every `f.geo.*` and every file type (`f.image`/`f.video`/
11
+ `f.audio`/`f.attachment`), `false` for everything else (text, int, decimal, bool, email,
12
+ enum, json, object, password, date, tableRef); inputs add date, timestamp and file, so an
13
+ omitted one binds `null` (a `required` one still accepts an explicit `null`). Pass `nullable` to override —
14
+ e.g. `f.geo.polygon({ nullable: false })`. An empty default becomes NULL only when
15
+ nullable, so a non-null `f.vector(8)` column fails to create (`''` is not a vector).
16
+ **`f.geo.*` values are `{ type, data }`, not GeoJSON.** The same shape goes in and comes
17
+ back. `type` per column: `point` → `"point"`, `data` `{ lng, lat }`; `multipoint` → `"points"`,
18
+ `linestring` → `"path"`, `polygon` → `"poly"` (ring closed for you), `data` `[{ lng, lat }, …]`;
19
+ `multilinestring` → `"paths"`, `multipolygon` → `"polys"`, `data` `[[{ lng, lat }, …], …]`.
20
+ Any other spelling (`"multipoint"`, `"linestring"`, `"Point"`) 400s `Only strings are supported for:`.
21
+ Raw WKT text (`c.text("POINT(1 2)")`) is accepted on write too, but a read never returns one.
22
+ `methods` (validators/transforms, names per type below): `"trim"`, `"min:8"`
23
+ (a text arg keeps its `:`: `"startsWith:https://"`), or `{ name, arg }` — for unlisted names and `pattern`'s error text.
24
+ `f.json({children})` declares the nested shape stored INSIDE a json column — an ARRAY of
25
+ `{name, type, methods?, children?}`, order-significant, distinct from the `FieldMap` that
26
+ `f.object` takes positionally. Omit it for an unstructured json column.
27
+ `f.enum(values)`/`f.vector(size)`/`f.object(children)`/`f.tableRef(table)` take a
28
+ positional payload before opts — and still accept the standard `FieldOptions` after it
29
+ (`f.enum([])`/`input.enum([])` are accepted: a pulled enum with no options, not one to
30
+ author — it brands the column `never`, which `InferRow` surfaces as `undefined`.)
31
+ (e.g. `f.tableRef(users, { required: true })` — only `min`/`max` are listed as tableRef
32
+ methods below, but `required`/`nullable`/`description`/… apply like any field.)
33
+ An **OPTIONAL foreign key wants a `0` sentinel, not `nullable: true`**. `f.tableRef` to an int-keyed table stores
34
+ an `int`, and a null in it is unqueryable: `null` is never a legal `fieldValue`/`id`, so
35
+ `s.db.get`/`edit`/`del` on that column answer HTTP 400 `Missing param: field_value` rather
36
+ than matching nothing. Declare `f.tableRef(users, { required: true, default: 0 })` for
37
+ "not set yet" — `s.db.get({ fieldName: "driver", fieldValue: c.int(0) })` matches no row and
38
+ binds `null`, which is the answer the null was reaching for. `export()` warns on a literal
39
+ `c.null()` in that slot. A uuid-keyed ref stays nullable even when `required` (a non-null one defaults to `""`, which no insert accepts).
40
+ An `f.vector(size)` column is SEARCHED through `s.db.query`'s `eval` pipeline, not through
41
+ any `SearchOp`: give the table `index: [{ type: "vector", fields: [{ name: "embedding", op:
42
+ "vector_cosine_ops" }] }]`, then rank with a distance filter + a sort on its alias (see
43
+ `s.db.query` → `eval`). Without that pairing the column stores and indexes but nothing
44
+ queries it.
45
+ `{ array: true }` makes any `f.*` scalar a **list column** — `f.text({ array: true })`
46
+ surfaces as `string[]` in `InferRow<typeof table>` (the column analogue of `input.list`).
47
+ **Seed a table's starting rows with `table({ seed })`.** `seed` takes rows
48
+ typed against the table's schema as a WRITE shape (a column without
49
+ `required: true`, and the system columns, may be omitted; `null` needs
50
+ `nullable: true`) — inline (`seed: [{ name: "…" }]`), a FILE
51
+ (`seed: seedFile("./seed.json", import.meta.url)`; path resolves against the DECLARING
52
+ file), or a thunk (`seed: () => import("./seed.json")`, async ok, `.default` unwrapped).
53
+ Inline rows are TYPED against the schema at compile time; a `seedFile`/thunk seed is
54
+ not — `xanosdk export`/`deploy` validates it, naming the row index and column.
55
+ Timestamp seeds: epoch ms, `Date`, ISO 8601 date, or date-time with `Z`/offset; else THROWS.
56
+ Int seeds: whole, in int64 (past 2^53: a string).
57
+ ⚠ Prefer `seedFile` for a file: a thunk's `import()` sits in
58
+ YOUR module, so a bundler emits the JSON as a served chunk. Never put secrets
59
+ in `seed`. `deploy --static` REFUSES a build carrying internal/sensitive seed values;
60
+ declare public ones (demo logins) on the table: `publicSeed: ["password"]`.
61
+ A replacing deploy re-seeds cleanly; `--keep-data` writes no seed rows.
62
+ `seed` is evaluated on EVERY build, so a computed value (`Date.now()`) is fine:
63
+ the lock records no seed data.
64
+ A FILE column seeds from a repo file: `logo: hostedFile("./logo.png", import.meta.url)`;
65
+ the row stores each backend's own copy. A wrong-kind file or a non-file column THROWS.
66
+ A release carries no file bytes: `release create` refuses such rows and icons.
67
+ Omit `id` and rows auto-number `1..N` (int PK) or take
68
+ a stable derived uuid (uuid PK); supplying `id` pins it (and
69
+ resets an int sequence past the max). All-or-nothing — mixing explicit and
70
+ omitted `id` throws. A `system:false` PK is the author's to supply. Pinning is
71
+ `seed`-only — `s.db.bulk.add` DROPS `id` unless `allowIdField: true`.
72
+ **`use_xdo` storage mode.** Workspace setting (`registerWorkspace({ use_xdo })`,
73
+ default `false`): `true` stores fields as JSON in the `xdo` column (+ `gin(xdo)` index),
74
+ `false` real columns.
75
+ Tables inherit it; override with `table({ useXdo })`; a merge refuses to switch a populated table.
76
+ A **column `default` must stay within the BMP** — a 4-byte character (codepoint > U+FFFF,
77
+ e.g. an emoji) is rejected at export; put such a value on an `input.<type>({ default })`.
78
+ `input.*` mirrors `f.*` — every column type below is
79
+ a legal input (scalars, files `input.image/video/audio/attachment`, `input.geo.*`,
80
+ `input.vector(size)`, `input.tableRef(table)`, `input.object(children)`), plus
81
+ `input.dbLink(table)` is the odd one: ONE entry that EXPANDS into one input per
82
+ COLUMN of the linked table, so read them by column name (`inp("email")`), never by
83
+ the entry's own name. `hidden: ["created_at"]` drops columns from that expansion.
84
+ `input.list(element)` for arrays — wrap any element constructor, e.g.
85
+ `input.list(input.text())` or `input.list(input.object({ id: f.int() }))`.
86
+ **A file reaches a column in two steps.** `input.file` is the RAW upload (the request's
87
+ multipart/base64 bytes) and cannot be written to a file column directly: store it first —
88
+ `s.storage.create_image({ as: "img", value: inp("avatar"), access: "public" })`
89
+ (read the upload with `inp`; `ref("input.avatar")` spells a stack VARIABLE named
90
+ `input` and fails the request with `Missing var entry: input`) — then write
91
+ `ref("img")` into the `f.image()` cell with `s.db.add`/`edit`. `access` defaults to
92
+ `"public"` (a guessable URL): pass `"private"` and hand out `s.storage.sign_private_url`
93
+ results for anything user-scoped. `create_video`/`create_audio`/`create_attachment` are
94
+ the same shape for the other file columns, each fed `input.file` (`input.image`/… 400s an upload).
95
+ **An empty file column is a stored null that `where` does not see as null.** `expr(col("file"), "!=", c.null())`
96
+ matches EVERY row, and `= c.null()` matches none. Compare a key inside the file instead:
97
+ `expr(col("file.path"), "!=", c.null())` keeps only the rows holding a file.
98
+ **Typed inputs validate/coerce on bind, before your stack runs** — so reach for the
99
+ specific type instead of hand-rolling checks. `input.email({ required: true })` rejects a
100
+ malformed address with a 400 (and trims; add `methods: ["lower"]` to downcase) — no
101
+ `regex_matches` needed; `input.int`/`input.decimal`/`input.uuid`/`input.enum([...])`/`input.date`
102
+ likewise reject or coerce bad input at the boundary. Drop to `input.text` + `s.precondition`
103
+ only for rules no type expresses.
104
+ ⚠ `input.url` is NOT one of them — there is no engine `url` type, so it stores as `text`
105
+ and validates NOTHING: a `javascript:`/`data:` URL type-checks, imports, and binds. When the value gets navigated
106
+ to, check the scheme in the stack. It is INPUT ONLY — there is no `f.url` column.
107
+ ⚠ `input.timestamp` binds epoch MILLISECONDS. A number is taken as ms as-is, so epoch
108
+ SECONDS (`date +%s`) bind in January 1970 with no error. An ISO-8601 string keeps its
109
+ offset but DROPS fractional seconds (`"…T19:30:00.123Z"` binds `…:00.000`). Send epoch ms
110
+ (`Date.now()`, `getTime()`) whenever sub-second precision matters.
111
+ ⚠ `input.list` does NOT reject a non-array: one value binds as a ONE-item list (`"a"` →
112
+ `["a"]`), and a non-JSON string given to an `input.object` binds an object of its
113
+ children's DEFAULTS — so `input.list(input.object({...}))` given `"notalist"` binds
114
+ `[{ name: "", … }]` with a 200. A list `min` does not catch it (that is one item). A string
115
+ starting with `[` is parsed as JSON with NO element check at all. When the shape must hold,
116
+ mark an object child `required: true` (the default-filled object then 400s with its param
117
+ path) and check anything else with `s.precondition`.
118
+ ⚠ `s.precondition`'s `error` must be a TAGGED value — `c.text("…")`, not a bare string.
119
+ The engine falls back to the generic "Precondition failed." whenever it reads an empty or
120
+ non-scalar message, and a bare string lands there, so the client never sees your text. The
121
+ `error_type` → HTTP status mapping is correct either way; only the message is lost.
122
+ `error_type` is how a stack sets a FAILURE status: `badrequest`/`inputerror` → 400, `unauthorized` → 401, `accessdenied` → 403, `notfound` → 404, `toomanyrequests` → 429, `standard` → 500.
123
+ For any other status — and for a redirect — use `respond.*`, which is sugar over
124
+ `s.util.set_header` (that statement reaches the status line): `respond.status(201)`,
125
+ `respond.redirect(url, { status?: 301|302|303|307 })` (a TUPLE — spread it; a `Location`
126
+ alone does NOT redirect), and `respond.header(name, value)`. Position in the stack does not
127
+ matter, a later status wins, and a FAILING `precondition` still wins over anything set
128
+ before it. ⚠ `204` answers with an empty body whatever `response` says. ⚠ `respond.status`
129
+ REFUSES `308`/`425`/`451`/`102`/`103`: the platform writes those and does not act on them,
130
+ so the response is 200 with nothing reported — use 301, 429 and 403 instead.
131
+ Normalizing transforms run on bind too — put `trim`/`lower`/`upper` on the input's `methods`
132
+ so `inp("name")` reads already-normalized; don't reroll `var $x = inp("name")|trim` in the stack.
133
+ ⚠ `f.decimal`'s size depends on the table's layout; it takes no precision or scale option.
134
+ On a normalized table (`useXdo: false`, the default) it is FIXED: 5 decimal places out
135
+ of 14 total digits. `0.12345678` written and re-read is `0.12346` (`-0.000004` → `0`),
136
+ and the rounding is silent at HTTP 200 — for a scalar `s.db.add`'s `as` output carries the
137
+ STORED value, not the cell you sent, so comparing the two detects the loss; for a list
138
+ (`array: true`) it is the unrounded list you sent, so only a re-read shows it. Rounding happens BEFORE
139
+ the size is checked, so a magnitude needing 10 digits left of the point AFTER rounding 500s
140
+ `ERROR_FATAL` `SQL Error: 0`, naming neither column nor value: `999999999.5` stores,
141
+ `999999999.999999` does not. On a `useXdo: true` table it is a JSON number with no 5dp
142
+ rounding (`0.123456789` reads back `0.123456789`). On either layout, for anything finer than 5dp
143
+ store an integer count of the smallest unit (cents, satoshis) in an `f.int`.
144
+ ⚠ `f.int` is a SIGNED 64-bit column — `-9223372036854775808` to `9223372036854775807` —
145
+ and past the ceiling it CLAMPS instead of failing: `9223372036854775808` and
146
+ `18446744073709551615` both store as `9223372036854775807`, at HTTP 200 with no error, so
147
+ an overflowed count reads back as a plausible number. The response carries every digit, but
148
+ `JSON.parse` (so `res.json()`) rounds past `9007199254740991` — read a count that large as
149
+ text or BigInt. `c.int()` refuses an unsafe number literal, so pass the string.
150
+
151
+ - `f.text` — methods: alphaOk, digitOk, lower, max, min, ok, pattern, startsWith, trim, upper
152
+ - `f.int` — methods: max, min
153
+ - `f.decimal` — methods: max, min
154
+ - `f.bool`
155
+ - `f.uuid`
156
+ - `f.date`
157
+ - `f.email` — methods: lower, trim
158
+ - `f.password` — methods: max, min, minAlpha, minDigit, minLowerAlpha, minSymbol, minUpperAlpha, salt
159
+ - `f.json`
160
+ - `f.timestamp` (stored `epochms`)
161
+ - `f.image` (stored `blob_img`)
162
+ - `f.video` (stored `blob_video`)
163
+ - `f.audio` (stored `blob_audio`)
164
+ - `f.attachment` (stored `blob`)
165
+ - `input.file` — INPUT ONLY (no `f.` form)
166
+ - `input.dbLink` (stored `<tableGuid>_mvpschema`) — INPUT ONLY (no `f.` form)
167
+ - `f.geo.point` (stored `geo_point`)
168
+ - `f.geo.multipoint` (stored `geo_multipoint`)
169
+ - `f.geo.linestring` (stored `geo_linestring`)
170
+ - `f.geo.multilinestring` (stored `geo_multilinestring`)
171
+ - `f.geo.polygon` (stored `geo_polygon`)
172
+ - `f.geo.multipolygon` (stored `geo_multipolygon`)
173
+ - `f.enum`
174
+ - `f.vector` — methods: max, min
175
+ - `f.object` (stored `obj`)
176
+ - `f.tableRef` (stored `int`) — methods: max, min
@@ -0,0 +1,183 @@
1
+ # Filter catalog
2
+
3
+ > Read when piping a value through `fl.*` — the full catalog with each filter's argument types.
4
+
5
+ Attach to a value with `withFilters(v, fl.name(...))` — the value `filters[]`
6
+ pipeline. Filters are passed spread (canonical); the array form
7
+ `withFilters(v, [fl.a(), fl.b()])` is also accepted. Every filter is typed:
8
+ the ones below carry named args, and the rest take NO arguments — call them
9
+ `fl.<name>()`, and passing an argument is a compile error.
10
+ A typed filter's declared argument list is an EXACT count, not a floor: passing more
11
+ than it lists THROWS, in the type and at runtime. The extra argument used to ride into
12
+ the filter's arg list, where the engine either ignores it or fails opaquely on a live
13
+ endpoint. `filter("name", …)` is the untyped escape for a filter the catalog
14
+ under-declares. Seven declared filters are exceptions the engine really does take more from, so they
15
+ stay variadic: `concat` (trailing separator), `index_by` (trailing list flag), `get` (trailing variable
16
+ map), `array_merge` and `array_merge_recursive` (any number of arrays), `jwe_encode` and
17
+ `jwe_decode`. To send more than a zero-argument filter's `()` accepts, use the same
18
+ `filter("name", …)` escape. A filter with NO declared list takes nothing and is emitted
19
+ `()` — except the variadic few listed under the catalog, which take arguments the
20
+ catalog never declared and so are not arity-checked at all.
21
+ A typed filter also accepts one object of NAMED arguments (`fl.add({ value: 1 })`); a key
22
+ the filter does not declare throws rather than being dropped.
23
+ A bare JS **scalar** — string, number or boolean — is accepted in ANY `fl.*` argument and
24
+ wrapped as the constant you would have written by hand (`fl.get("a.b", 0)` encodes
25
+ identically to `fl.get(c.text("a.b"), c.int(0))`). An object or array must still be built
26
+ with `c.obj`/`c.array`. Any argument also takes a runtime value (`inp`, `ref`, a
27
+ `withFilters` chain) — inside `obj()`/`expr()` an argument cannot carry its own filter
28
+ chain, so compute it in a prior `set_var`. The arg types below name each argument's
29
+ ENGINE type, not the JS type you may pass.
30
+ To add to a numeric column (counter, stock, balance), use `s.db.increment`
31
+ (`llms/statements-data.md`) — one atomic UPDATE. A pipeline read-modify-write is NOT
32
+ atomic (concurrent writers lose updates), and `col("clicks")` does NOT resolve to the
33
+ stored value inside a `db.edit` `row` (it is `null`; `null + 1` aborts): `db.get` the row
34
+ `as: "current"`, then `withFilters(ref("current.clicks"), fl.add(c.int(1)))`.
35
+
36
+ - `fl.add(value: decimal): decimal`
37
+ - `fl.append(value: <T>, path: text): <T>[]`
38
+ - `fl.array_diff(value: <T>[]): <T>[]`
39
+ - `fl.array_diff_assoc(value: <T>[]): <T>[]`
40
+ - `fl.array_fill(start: int, count: int): any[]`
41
+ - `fl.array_fill_keys(keys: any[]): any[]`
42
+ - `fl.array_filter(match: obj, path?: text): <T>[]` — matches by EXAMPLE (an object of key/value pairs), not by code, and compares LOOSELY
43
+ - `fl.array_find(match: obj): <T>` — a MISS THROWS (`Unable to find element.`) rather than yielding null — use `array_has` to test first
44
+ - `fl.array_has(match: obj): bool`
45
+ - `fl.array_intersect(value: <T>[]): <T>[]`
46
+ - `fl.array_intersect_assoc(value: <T>[]): <T>[]`
47
+ - `fl.array_merge(value: <T>[]): <T>[]`
48
+ - `fl.array_merge_recursive(value: <T>[]): <T>[]`
49
+ - `fl.array_push(value: <T>): <T>[]`
50
+ - `fl.array_push_conditional(value: any, conditional: bool): <T>[]`
51
+ - `fl.array_remove(value: <T>, path: text, strict?: bool): <T>[]`
52
+ - `fl.array_replace(match: any, result: any): <T>[]`
53
+ - `fl.array_set(path: text, value: any): obj` — sets a DOTTED path and returns the value; a non-object piped value is replaced by an empty one
54
+ - `fl.array_slice(offset: int, length?: int): <T>[]`
55
+ - `fl.array_unshift(value: <T>): <T>[]`
56
+ - `fl.array_upmerge(match: obj, node: obj): <T>[]` — merges the node INTO each matched element; `array_replace` swaps the element whole
57
+ - `fl.base_convert(from_base: any, to_base: any): text`
58
+ - `fl.bitwise_and(value: int): int`
59
+ - `fl.bitwise_or(value: int): int`
60
+ - `fl.bitwise_xor(value: int): int`
61
+ - `fl.clamp_max(max: decimal): decimal`
62
+ - `fl.clamp_min(min: decimal): decimal`
63
+ - `fl.concat(value: any): text`
64
+ - `fl.contains(search: text): bool` — piped value is the subject text; the arg is the substring searched for
65
+ - `fl.convert_encoding(to: text, from: text): text`
66
+ - `fl.create_object(values: <T>[]): any`
67
+ - `fl.crypto_jwe_decode(check_claims: json, key: json, key_algorithm: enum, content_algorithm: enum, timeDrift?: int): json`
68
+ - `fl.crypto_jwe_encode(headers: json, key: json, key_algorithm: enum, content_algorithm: enum, ttl?: int): text`
69
+ - `fl.crypto_jws_decode(check_claims: json, key: json, algorithm: enum, timeDrift?: int): json`
70
+ - `fl.crypto_jws_encode(headers: json, key: json, algorithm: enum, ttl?: int): text`
71
+ - `fl.csv_create(rows: text[], separator: text, enclosure: text, escape: text): text` — the header-writing counterpart to `csv_encode`: the PIPED value is the list of column names (written as the header line) and `rows` carries the data rows
72
+ - `fl.csv_decode(separator: text, enclosure: text, escape: text): any`
73
+ - `fl.csv_encode(separator: text, enclosure: text, escape: text): text` — writes NO header — values only, each row in THAT row's key order with no normalization across rows, so rows whose keys differ in order or count silently misalign columns. Nested cells are JSON-encoded and `false` writes empty. A piped array of SCALARS is treated as one row. Use `csv_create` for a header
74
+ - `fl.csv_parse(separator: text, enclosure: text, escape: text): any`
75
+ - `fl.decrypt(algorithm: enum, key: text, iv: text): text`
76
+ - `fl.detect_encoding(encodings?: text): text`
77
+ - `fl.div(value: decimal): decimal`
78
+ - `fl.encrypt(algorithm: enum, key: text, iv: text): text`
79
+ - `fl.ends_with(search: text): bool` — piped value is the subject text; the arg is the substring searched for
80
+ - `fl.epochms_add_ms(milliseconds: int): epochms`
81
+ - `fl.epochms_add_secs(seconds: int): epochms`
82
+ - `fl.epochms_date(format: text, timezone?: text): text`
83
+ - `fl.epochms_from_format(format: text, timezone?: text): text`
84
+ - `fl.epochms_transform(format: text, timezone?: text): text` — applies a relative shift (e.g. "+1 day") to the timestamp
85
+ - `fl.eq(value: <T>): bool`
86
+ - `fl.every(code: text, timeout?: int): any[]` — `code` is a JS body run per element (true for all?), over `$this`/`$index`/`$parent`
87
+ - `fl.filter(code: text, timeout?: int): any[]` — `code` is a JS body run per element (keep it? true/false), over `$this`/`$index`/`$parent`
88
+ - `fl.filter_empty(path?: text): <T>[]` — keeps entries that are not empty ("", null, 0, "0", false, [], {})
89
+ - `fl.filter_empty_array(path?: text): <T>[]`
90
+ - `fl.filter_empty_object(path?: text): <T>[]`
91
+ - `fl.filter_empty_text(path?: text): <T>[]`
92
+ - `fl.filter_false(path?: text): <T>[]`
93
+ - `fl.filter_null(path?: text): <T>[]`
94
+ - `fl.filter_zero(path?: text): <T>[]`
95
+ - `fl.find(code: text, timeout?: int): any[]` — `code` is a JS body run per element; returns the first element it accepts
96
+ - `fl.findIndex(code: text, timeout?: int): any[]` — `code` is a JS body run per element; returns the first matching index
97
+ - `fl.first_notempty(value: any): any` — first value that is not empty ("", null, 0, "0", false, [], {})
98
+ - `fl.first_notnull(value: any): any`
99
+ - `fl.fsort(path?: text, type?: "text"|"itext"|"natural"|"inatural"|"number", asc?: bool): <T>[]` — `type` is the comparator, and ONLY "number" compares numerically — "text"/"itext" are strcmp/strcasecmp, "natural"/"inatural" are the human-readable "a2 < a10" orderings. Default "itext". Anything else silently sorts as text, so a numeric sort MUST spell "number"; the path arg drills into each element
100
+ - `fl.get(path: text, default?: json): any` — `path` may be a runtime value: after `fl.index_by("suite_id")`, `fl.get(ref("suite.id"))` returns that key's rows (null for a missing key)
101
+ - `fl.gt(value: <T>): bool`
102
+ - `fl.gte(value: <T>): bool`
103
+ - `fl.has(path: text): bool`
104
+ - `fl.hmac_md5(key: text, raw?: bool): text`
105
+ - `fl.hmac_sha1(key: text, raw?: bool): text`
106
+ - `fl.hmac_sha256(key: text, raw?: bool): text`
107
+ - `fl.hmac_sha384(key: text, raw?: bool): text`
108
+ - `fl.hmac_sha512(key: text, raw?: bool): text`
109
+ - `fl.icontains(search: text): bool` — case-insensitive; piped value is the subject, the arg is the substring
110
+ - `fl.iends_with(search: text): bool` — case-insensitive; piped value is the subject, the arg is the substring
111
+ - `fl.in(search: <T>): bool`
112
+ - `fl.index_by(path: text): { [key: string]: <T>[] }` — a GROUP-BY: every value is an ARRAY of the items sharing that key, even when only one does, so a lookup reads `idx[key][0]`. Items whose path is missing or non-scalar are dropped
113
+ - `fl.istarts_with(search: text): bool` — case-insensitive; piped value is the subject, the arg is the substring
114
+ - `fl.join(separator: text): text`
115
+ - `fl.jwe_decode(arg1: any)`
116
+ - `fl.jwe_encode(arg1: any)`
117
+ - `fl.lambda(code: text, timeout?: int): any` — runs a JS body once over the piped value, which it binds as `$this` (NOT `$parent`)
118
+ - `fl.log(base: <T>): decimal`
119
+ - `fl.lt(value: <T>): bool`
120
+ - `fl.lte(value: <T>): bool`
121
+ - `fl.ltrim(mask?: text): text`
122
+ - `fl.map(code: text, timeout?: int): any[]` — `code` is a JS body run per element, over `$this`/`$index`/`$parent` — build it with `lam.fn`
123
+ - `fl.max(bound?: decimal): decimal`
124
+ - `fl.md5(raw?: bool): text`
125
+ - `fl.min(bound?: decimal): decimal`
126
+ - `fl.mod(value: int): int`
127
+ - `fl.mul(value: decimal): decimal`
128
+ - `fl.ne(value: <T>): bool`
129
+ - `fl.num_max(value: any): decimal`
130
+ - `fl.num_min(value: any): decimal`
131
+ - `fl.number_format(decimals: int, decimal_separator: text, thousands_separator: text): string`
132
+ - `fl.pick(keys: text): <T>`
133
+ - `fl.pow(exp: <T>): decimal`
134
+ - `fl.prepend(value: <T>, path: text): <T>[]`
135
+ - `fl.range(start: int, stop: int): int[]`
136
+ - `fl.reduce(initial_value: int, code: text, timeout?: int): any[]` — `code` is a JS body run per element; the ACCUMULATOR is `$result` (there is no `$acc`) and `initial_value` is REQUIRED — omitting it would slot the code as the initial value
137
+ - `fl.regex_match(subject: text): text[]` — piped value is the PATTERN, the arg is the subject — see `regex_test`
138
+ - `fl.regex_match_all(subject: text): text[]` — piped value is the PATTERN, the arg is the subject — see `regex_test`
139
+ - `fl.regex_quote(delimiter?: text): text`
140
+ - `fl.regex_replace(replacement: text, subject: text): text` — piped value is the PATTERN, `subject` is the text searched — see `regex_test`. The replacement comes FIRST
141
+ - `fl.regex_test(subject: text): bool` — piped value is the PATTERN (build it with `c.regex(...)`); the arg is the subject — the REVERSE of `contains`/`starts_with`. Swapped, it answers false for every input with no error, so write `withFilters(c.regex("^a+$"), fl.regex_test(inp("s")))` (or name the arg: `fl.regex_test({ subject: inp("s") })`). A pattern found in the subject slot is refused at build time
142
+ - `fl.round(precision?: int): decimal`
143
+ - `fl.rtrim(mask?: text): text`
144
+ - `fl.secureid_decode(salt: text): int`
145
+ - `fl.secureid_encode(salt: text): text`
146
+ - `fl.set(path: text, value: any): any`
147
+ - `fl.set_conditional(path: text, value: any, conditional: any): any`
148
+ - `fl.set_ifnotempty(path: text, value: any): any`
149
+ - `fl.set_ifnotnull(path: text, value: any): any`
150
+ - `fl.sha1(raw?: bool): text`
151
+ - `fl.sha256(raw?: bool): text`
152
+ - `fl.sha384(raw?: bool): text`
153
+ - `fl.sha512(raw?: bool): text`
154
+ - `fl.some(code: text, timeout?: int): any[]` — `code` is a JS body run per element (true for any?), over `$this`/`$index`/`$parent`
155
+ - `fl.splice(offset: int, length?: int, path?: text): <T>[]` — returns the REMOVED slice, not the remainder — [1,2,3,4] spliced at offset 1 length 2 yields [2,3]; with a `path` it removes in place and returns the whole value instead
156
+ - `fl.split(separator: text): text[]`
157
+ - `fl.starts_with(search: text): bool` — piped value is the subject text; the arg is the substring searched for
158
+ - `fl.string_replace(search: text, replacement: text): text`
159
+ - `fl.strip_tags(exclude?: text): text`
160
+ - `fl.stripos(search: text): int`
161
+ - `fl.strpos(search: text): int`
162
+ - `fl.sub(value: decimal): decimal`
163
+ - `fl.substr(start: int, length: int): text`
164
+ - `fl.to_epoch_day(timezone?: text): int`
165
+ - `fl.to_epoch_hour(timezone?: text): int`
166
+ - `fl.to_epoch_minute(timezone?: text): int`
167
+ - `fl.to_epoch_ms(timezone?: text): int`
168
+ - `fl.to_epoch_sec(timezone?: text): int`
169
+ - `fl.to_epochms(timezone?: text): epochms`
170
+ - `fl.transform(expression: text): any` — `expression` is Xano Expression Engine source, NOT a JS body — no `return`, and the piped value is `$0` (or `$$`), NOT `$this` (which is null here). `$var`/`$input`/`$env`/`$auth` resolve and filters pipe inside it: `$0 * 2`, `$0|sort|join:","`. Parenthesize a pipe inside an object literal — `{ s: ($0|sort|join:",") }` — or its comma is read as the key separator and later keys vanish silently. For JavaScript use `lambda`
171
+ - `fl.trim(mask?: text): text`
172
+ - `fl.unique(path?: text): <T>[]`
173
+ - `fl.unpick(keys: text): <T>` — returns the object without the named keys (inverse of a pick)
174
+ - `fl.unset(path: text): any`
175
+ - `fl.url_addarg(key: text, value: text, encoding_rfc3986?: bool): text`
176
+ - `fl.url_delarg(key: text): text`
177
+ - `fl.url_getarg(key: text, default?: text): text`
178
+ - `fl.url_hasarg(key: text): text`
179
+ - `fl.yaml_encode(options?: bool, inline?: int, indent?: int): text`
180
+
181
+ Zero-argument filters — call as `fl.<name>()`; an argument is a compile error: abs, acos, acosh, addslashes, array_entries, array_keys, array_lower, array_pop, array_shift, array_shuffle, array_values, asin, asinh, atan, atanh, avg, base64_decode, base64_decode_urlsafe, base64_encode, base64_encode_urlsafe, bin2hex, bindec, bitwise_not, capitalize, ceil, cos, count, create_object_from_entries, decbin, dechex, decoct, deg2rad, empty, escape, even, exp, first, flatten, floor, from_utf8, hex2bin, hexdec, is_array, is_bool, is_decimal, is_int, is_object, is_text, is_uuid, json_decode, json_encode, last, list_encodings, ln, log10, lower, ms_to_secs, not, null, octdec, odd, product, querystring_parse, rad2deg, reverse, rsort, safe_array, secs_to_ms, sin, sort, sql_alias, sql_esc, sqrt, strip_accents, strlen, sum, tan, text_escape, text_unescape, to_bool, to_decimal, to_expr, to_geo_linestring, to_geo_multilinestring, to_geo_multipoint, to_geo_multipolygon, to_geo_point, to_geo_polygon, to_int, to_text, to_utf8, uid, upper, url_decode, url_decode_rfc3986, url_encode, url_encode_rfc3986, url_parse, uuid4, xml_decode, yaml_decode.
182
+
183
+ Variadic filters — they take arguments, but no declared list, so the count is not checked: sprintf.
@@ -0,0 +1,30 @@
1
+ # Agent and MCP def shapes
2
+
3
+ > Read when defining an `agent()`, `mcpServer()`, `prompt()` or `resource()`.
4
+
5
+ - `mcpServer({ name, guid?, description?, instructions?, docs?, enabled?, canonical?, spec?, tags?, history?, tools?, prompts?, resources?, oauth?, llm?, output? })` — an MCP toolset. `llm?`/`output?` are the same blocks `agent()` takes and are usually absent: an MCP server and an agent are ONE stored object distinguished by `type`. Returns a handle with `getPath()`/`getUrl(baseUrl)` for the Streamable-HTTP endpoint — `getUrl` is NOT idempotent, a `baseUrl` already carrying an `/x2/mcp/<…>/stream` path THROWS, so resolve ONCE from the instance base URL. Fires on an ephemeral. A TENANT rides AFTER the prefix (`/x2/mcp/tenant/<tenant>/<canonical>/<token>/stream`), not as a base-URL segment: `getUrl` LIFTS a `/tenant/<name>` out of the base and re-places it there, so `getUrl(window.XANO_HOST)` is correct as-is; concatenating them by hand builds an unregistered route that 404s. Pass `{ tenant }` when the base URL cannot name it (a tenant on its own domain).
6
+ - `tools?`: a `ToolsetToolEntry[]`. Pass the `tool()` HANDLES directly (`tools: [saveNote]`); use the `{ tool, enabled?, auth?, type?, resourceUri?, toolMeta? }` wrapper when an entry needs more. `type: "resource"` (+ a required `resourceUri`) is the LEGACY way to publish a tool as an MCP resource — author a `resource()` and list it in `resources` instead; `toolMeta` is extra metadata on a tool entry. The two payload fields are per-type and supplying the other one is refused, because the engine stores it against neither. `auth` names an auth **table** (a `table({ auth: true })` handle or its name) — per-tool, and the only gate there is. ⚠ It is also what gives the tool a CALLER, on an `agent()` exactly as on an MCP server. `auth()` inside a tool whose entry names no table does NOT return null: the call answers a normal result carrying `{"code":"ERROR_FATAL","message":""}`, so the model reads a successful tool result and reports success while nothing is written. A caller token does not rescue it — the ENTRY supplies identity. Spell it `{ tool: saveNote, auth: users }`; `export()` warns. An entry naming no tool THROWS at export; a deliberate raw `id: 0` warns and carries through.
7
+ - `prompts?` / `resources?`: `prompt()` / `resource()` HANDLES (or names), or a `{ prompt, enabled?, auth? }` / `{ resource, enabled?, auth? }` wrapper. `auth` names the auth table and is what gives the stack a caller: a prompt/resource reading `auth()` behind an entry with none answers every `prompts/get` / `resources/read` with `-32603 "Access Denied"` (`export()` warns `toolset.primitive-reads-auth-ungated`). Listing one target twice THROWS; two resources with one `uri` on one server fail `export()` (`mcp.resource-uri-duplicate`). MCP-server only — `agent()` takes tools only.
8
+ - `history?` is the server tier for its tools, prompts AND resources alike; an object's own `history` wins. The workspace tier (`workspaceConfig({ history })`) has `prompt` and `resource` keys, both default on.
9
+ - `oauth?`: per-server sign-in; omit and nothing changes. Union on `mode`; `authTable` (REQUIRED, auth `table()` handle or name) is whose row the signed-in user is — `auth()` on the whole server, `t.auth` in its trigger. Hosted (the platform is the authorization server): `{ mode: "hosted", authTable, loginUrl }` — clients are sent to `loginUrl` (YOUR page, which is also the consent screen) with `mcp_request`; show `s.mcp.oauth.request`, sign the user in, then your endpoint calls `s.mcp.oauth.complete` with their `decision` and the page redirects to the URL it binds. External (your IdP issues tokens): `{ mode: "external", authTable, issuer, column, audience?, audienceAck?, claim?, claimAck?, preset?, allowedClientIds? }` — token `claim` (default `sub`) matched against `column`; `preset` is `auth0|clerk|workos|entra|okta|cognito|generic`. `loginUrl`/`issuer`/`audience` take `env("NAME")` (resolved per environment).
10
+ - `export()` FAILS: hosted without `loginUrl` (`mcp.oauth-hosted-incomplete`); external without `issuer`/`column` (`mcp.oauth-external-incomplete`); `preset: "clerk"` without `allowedClientIds` (`mcp.oauth-preset-client-ids`); `claim` other than `sub`/`oid`/`email` without `claimAck` (`mcp.oauth-claim-unacked` — users can edit it at most providers). Unknown `mode`/`preset` THROWS; so do the dropped `consent`/`branding`/`trustedClients`.
11
+ - `prompt({ name, guid?, description?, docs?, title?, icons?, tags?, history?, input?, stack?, response?, responseShape?, middleware? })` — an MCP prompt; takes `registerPrompts`. `input` is the prompt's arguments (each field's `description`/`required`/enum `values` reach the client; enum values also answer completion requests). `response`: a text value is ONE user message; a list of `{ role: "user" | "assistant", content }` is sent in order: `s.set_var("m", obj({ list: [obj({ role: c.text("user"), content: ref("body") })] }))`, `response: ref("m.list")`. Without `response` or a top-level `s.return`, `prompts/get` fails.
12
+ - `resource({ name, uri, guid?, description?, docs?, mimeType?, title?, icons?, annotations?, tags?, history?, input?, stack?, response?, responseShape?, middleware? })` — an MCP resource; takes `registerResources`. Clients address it by `uri`, never `name`.
13
+ - `uri`: a literal (`"docs://readme"`, no `input`) or an RFC 6570 LEVEL-1 template (`"orders://{region}/{order_id}"`). It must start with a scheme; each `{variable}` is letters/digits/`_`, one per brace pair, never two adjacent. `input` must be EXACTLY the template's variables, each a single `text`/`int`/`decimal`/`bool`/`enum`/`email`/`uuid` — anything else THROWS. A value that does not convert is refused before the stack runs.
14
+ - `response`: a string is returned as text; non-UTF-8 content (a file, binary) as a base64 blob. `mimeType?` omitted → derived from what the stack returns.
15
+ - `annotations?`: `{ audience?: ("user" | "assistant")[], priority?: 0..1, lastModified?: ISO 8601 string }`.
16
+ - MCP metadata, on `tool`/`prompt`/`resource`: `title?` (display name clients show instead of `name`); `icons?: { src, mimeType?, sizes?, theme? }[]` — `src` is `https:` (≤ 2048 chars), a base64 `data:` URI of PNG/JPEG/WebP (≤ 64 KiB), or `hostedFile("./icon.png", import.meta.url)`, a repo file served at each backend's own absolute URL (a pull keeps the `/vault/…` path); SVG, `http:` and every other scheme THROW; `mimeType` is `image/png|jpeg|webp`, `sizes` a list (`["48x48"]`, `["any"]`), `theme` `light|dark`. Tool only: `annotations?: { readOnlyHint?, destructiveHint?, idempotentHint?, openWorldHint? }` — hints; an unset one is derived by the platform — and `output?`, a structured-output schema in the `input` grammar: a returned plain object then reaches the client as `structuredContent` plus a text copy, and a result that does not match is a tool error naming the field.
17
+ - `s.mcp.elicit({ key, message, input?, as? })` — ask the MCP client's user mid-call; binds `{ action: "accept" | "decline" | "cancel", content? }` (`content` only on accept, keyed by the `input` field names). `key`: a stable name unique in the stack; `message`: string or value. `input` is the form: a FLAT set of `text`/`email`/`date`/`int`/`decimal`/`bool`/`enum` fields (an enum may be a list); nested objects, files, JSON, timestamps, `password` and `sensitive` fields THROW. Works in tool, prompt and resource stacks.
18
+ - ⚠ The WHOLE stack re-runs from the top when the user answers — everything BEFORE an elicit runs once per round trip, so ASK FIRST, then act. A write before an elicit (`s.db.*` writes, raw SQL, a non-GET `s.api.request`, `s.util.send_email`, `s.function.run`/`.call`, `s.tool.call`) makes `export()` warn `mcp.write-before-elicit`; accept a deliberate one per def with `diagnostics: { allow: ["mcp.write-before-elicit"] }`. An elicit inside `s.db.transaction` commits the transaction's earlier writes on every round trip.
19
+ - Input is collected only from clients on MCP `2026-07-28` or later that declare form elicitation. Every other caller — an older client, a debug run, an agent, a workflow test — gets `{ action: "cancel" }` and the stack carries on, so ALWAYS handle `cancel`. A runtime error inside an async function run.
20
+ - `s.mcp.progress({ progress, total?, message? })` — report progress to a client that asked for it (a no-op otherwise); values never go backwards (a lower `progress` is sent as the last one).
21
+ - `s.mcp.oauth.complete({ request, decision, unsafe_user_id?, as? })` — run by the endpoint your `loginUrl` page calls after sign-in; `request` is the page's `mcp_request`. Signs in the CALLER (`auth()`, an `authTable` row whose session token is under 10 minutes old) and binds the single-use URL (text) to redirect to. `decision` (`"approve" | "deny"`, REQUIRED) is what the user chose on your page. ⚠ `unsafe_user_id` names the row directly, skipping the caller checks — anyone reaching that endpoint signs in as that user.
22
+ - `s.mcp.oauth.request({ request, as? })` — a pending sign-in's verified details for your `loginUrl` page to show before the user approves: `{ client_name, client_logo, callback_host, client_verified, server_name }`. Does not consume it.
23
+ - `s.mcp.oauth.revoke({ mcp_server, user_id? | grant_id? | all?, as? })` — end hosted sign-ins on a server: EXACTLY ONE of `user_id`, `grant_id`, `all: c.bool(true)`, else `export()` fails (`mcp.oauth-revoke-target`). Binds `{ revoked }`.
24
+ - `agent({ name, guid?, description?, docs?, enabled?, canonical?, tags?, history?, llm, tools?, output? })` — an LLM orchestrator. No top-level `instructions`/`prompt`/`spec` — the prompt lives under `llm`. Invoke from a stack with `s.ai.agent.run({ agent, args })`.
25
+ - `llm` (REQUIRED): typed provider settings, a discriminated union on `type` (`"xano-free" | "anthropic" | "openai" | "google-genai"`). Shared fields: `systemPrompt?`, `maxSteps?` (default `5`), and `prompt?` XOR `messages?` (genuinely exclusive: both is a type error and throws — the engine stores ONE `prompt_type`, so one would be dropped); plus provider fields (`apiKey?`, `model?`, `temperature?`, `reasoningEffort?`, …). String fields accept Twig placeholders — `{{ $args.x }}` for run inputs (the `args` of `s.ai.agent.run`), `{{ $env.NAME }}` for env vars.
26
+ - `tools?`: same `ToolsetToolEntry[]` as `mcpServer` — bare `tool()` handles, or a `{ tool, enabled?, auth? }` wrapper when one needs `enabled`/`auth`. A tool reading `auth()` MUST use the wrapper naming the auth table — a bare handle runs it with no caller (see `mcpServer`'s `tools?`).
27
+ - `output?`: `{ schema: Record<string, input.*>, enabled? }` — structured-output schema. `schema` is a named-field record authored with the `input.*` catalog, exactly like a `defineFunction`/`query` `input:` map (the stored `structuredOutputsSchema` is the same wire shape as function inputs). e.g. `output: { schema: { priority: input.enum(["low","high"]), summary: input.text() } }`. When you pass the agent *handle* to `s.ai.agent.run({ agent })`, `.result` is typed straight from this schema — no `resultShape` witness needed (the shape is declared once). `resultShape` remains only to override that, or to type an agent referenced by bare name.
28
+ - **Run + read recipe (`s.ai.agent.run`):** bind the run to a var (`s.ai.agent.run({ agent, args, as: "run" })`) — it produces a rich envelope, and the completion is at **`.result`**. Read one structured field with a dotted ref (`response: ref("run.result.priority")`) or the whole completion (`ref("run.result")`, typed from `output.schema`); persist it in a later step the same way (`s.set_var("summary", ref("run.result.summary"))`). `args` is a plain object of run inputs (`{ topic: inp("topic") }` — raw literals are fine, e.g. `{ max_steps: 3 }`) surfaced to the agent as `{{ $args.topic }}`. `.toolCalls` is the FINAL step's calls only (often `[]`); every call is a `.steps[].content[]` entry of `type: "tool-call"`.
29
+ - **Marketplace:** for chat history, use `@xano-sdk/chatbot`; for vector RAG, use `@xano-sdk/vector` (`tools: [vector.searchTool]`). Find add-ons with `xanosdk marketplace search <q>`.
30
+ - Both kinds persist under the `toolset` payload key, so a same-name `agent` + `mcpServer` pair derives ONE guid and `export()` throws — give them distinct names.
@@ -0,0 +1,100 @@
1
+ # Core def shapes
2
+
3
+ > Read when authoring a function, query, api group, task, workflow test, middleware, tool, or addon — and for the `response` and `expr` shapes every one of them uses.
4
+
5
+ The def-object passed to each factory. `?` = optional. `input` is keyed by
6
+ input name (`input.<type>(opts?)`); `stack` is `Statement[]` (`s.*`); `response`
7
+ is a `ResponseDef` (see **Responses** below). Object identity is `guid?` —
8
+ omit it and it derives from `name` (for a query, from group + verb + name; set it
9
+ to survive a rename).
10
+
11
+ - `defineFunction({ name, guid?, description?, docs?, workspace?, input?, stack?, response?, tests? })`
12
+ - `query({ name, verb, apiGroup?, guid?, auth?, input?, stack?, response?, responseType?, apiEnabled?, disabled?, cache?, description?, docs?, tests?, example? })` — a `cache` setting turns caching on unless `active: false`
13
+ - `verb`: `"GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD"` (required), UPPERCASE. Anything else — most often a lowercase `"post"` — makes `query()` THROW, because Xano does NOT reject it: it stores the verb as NULL, a null verb serves as GET, and the endpoint then answers on the wrong method while the one you meant 404s `Unable to locate request.`
14
+ - `apiGroup`: an `apiGroup()` def handle (or its name) — binds by guid, stable across syncs. Raw numeric `apiGroupId?` is the escape hatch and wins if both given.
15
+ - `auth`: `false` (no auth) or an auth-table id; `responseType`: `"standard" | "stream"` (default `standard`) — any other spelling THROWS, since Xano stores an unrecognized one as NULL and a null buffers as `standard`, so a misspelled stream quietly does not stream.
16
+ - `name` is the endpoint PATH within the group.
17
+ - A `{param}` segment is a URL PATH PARAM bound to the input of the same name, and segments chain: `name: "blog/{slug}/review/{review_id}"` + `input: { slug: input.text(), review_id: input.int() }`. Read it with `inp("slug")` like any other input.
18
+ - Every `{param}` MUST have a matching input or `query()` THROWS — Xano treats an unbound marker as inert route text, so the endpoint would answer on the path and see nothing.
19
+ - A `{param}` need NOT be a whole segment (`"blog/post-{slug}"` routes fine), but its type must fit one segment (no object/list/json/file/geo/vector); there are no wildcards or patterns. `required: true` is NOT demanded (the engine's editor leaves path inputs unmarked).
20
+ - An input a `GET`/`DELETE`/`HEAD` looks ONE ROW up by (`s.db.get`/by-field edit/patch/delete; not `has` or an email/token/code) belongs in the path — `export()` warns `query.path-segment-candidate`: `getPath()` types STATIC. A segment is any value naming WHICH resource is wanted (`"shop/{country}"`); one that NARROWS A LIST (`s.db.query`) stays a query param. Intended? `diagnostics: { allow: [code] }` on the query (any def, for its own advisories) accepts it under `--strict`.
21
+ - Routes are matched FIRST-FIT in creation order, and a literal gets NO precedence over a `{param}`: `"runs/{id}"` (a text `id`) created before `"runs/trend"` answers `GET /runs/trend` itself. Two routes in one group and verb that one request path can match are REFUSED at `export()` (`query.route-shadowed`) — reordering `registerQueries` cannot fix it, since a release keeps an existing route first. Make the paths disjoint (`"runs/by-id/{id}"`), or declare a numeric param `input.int({ required: true })` (or `decimal`): only a REQUIRED int/decimal segment matches digits only — an OPTIONAL one matches any text, so `input.int()` still takes `trend`.
22
+ - Inputs absent from the path are ordinary query-string/body params.
23
+ - Name charset is ONLY `A-Za-z0-9_-/{}`, max 200: a `.` (`"export.zip"`) is NOT rejected by Xano — it stores an EMPTY name that deploys clean then 404s forever, so `query()` THROWS. Use `"export_zip"` and set the extension in the response headers.
24
+ - **Client recipe:** `q.getPath({ params: { slug: "hello" } })` → `/api:<canonical>/blog/hello` — never interpolate by hand.
25
+ - `getPath` percent-encodes each value (so `?`/`#`/spaces stay in their segment) and throws on what encoding cannot contain: a `/`, and a segment that becomes `.`/`..` as sent (a URL parser drops those before routing, addressing a different endpoint).
26
+ - The keys are typed from the literal `name`, so a typo is a compile error.
27
+ - `q.toSearchParams(input)` drops GET path params (free `query.toSearchParams` keeps them); lists send `k[]=a&k[]=b` (`k=a&k=b` binds only `b`).
28
+ - Browser bundle cost, the `routes.gen.ts` alternative, and spot-checking a def from Node: `llms/client.md`.
29
+ - `apiGroup({ name, guid?, canonical?, description?, docs?, swagger?, apiGroupEnabled?, documentation?, cors?, middleware? })` — a query container; queries bind to it via their `apiGroup`. `canonical` holds only letters, digits, `_` and `-` (it is the `<canonical>` of `/api:<canonical>`); anything else THROWS at build.
30
+ - `documentation?`: `{ require_token? }` — the gate on this group's hosted docs. `require_token: true` IS the declaration; the token lives in `xano/.secrets.json` under this group's guid, and a literal `token` FAILS the export. ⚠ ALWAYS emitted, present or not: an absent key is written as the engine default on import, so omitting the block CLEARS an existing gate (the opposite of the `workspaceConfig` rule). `swagger: true` publishes the docs; `require_token` gates them only alongside a non-empty token, so `swagger: true` with no gate makes the whole group readable by anyone with the URL — export warns once per build and `--strict` fails; `diagnostics: { allow: ["api-group.docs-public"] }` accepts it. A declared gate with NO token drops the block, so those bytes clear it: export FAILS under `swagger: true`, and with `swagger` off warns and completes.
31
+ - `cors?`: `{ mode?, allowOrigins?: string[], allowHeaders?: string[], allowCredentials?, maxAge?, allowMethods?: { get?, post?, put?, patch?, delete?, head? } }`.
32
+ - `mode?`: `"default"` (the default) | `"custom"` | `"disabled"`, lowercase — a fourth value THROWS at export (`apiGroup()` itself does not check), because Xano neither rejects nor blanks it: it DROPS THE WHOLE API GROUP on import, so the deploy succeeds and every query in the group 404s. ⚠ Every OTHER field applies only under `"custom"`: `"default"` serves a FIXED permissive policy (any origin, `allow-headers: *`, `allow-credentials: true`, `max-age: 86400`) and ignores the block, so setting `maxAge`/`allowCredentials`/`allowHeaders` alone changes nothing. `"disabled"` sends no CORS headers at all, so every browser call fails.
33
+ - ⚠ Under `"custom"`, `allowOrigins` is matched as EXACT strings (scheme+host+port, no wildcard or subdomain expansion) and `"*"` is compared as a literal origin — it matches NOTHING. An unmatched origin gets no `access-control-*` headers at all, so the browser call fails while export, deploy and the preflight look fine. Name each origin, or use `mode: "default"` for any-origin. `allowMethods` gates the REAL response too: a verb left off gets no CORS headers back even though its preflight passes. Export warns on an empty origin list, a `"*"` entry, and a policy with no method enabled; a non-origin entry (trailing `/`, path) is refused.
34
+ - Tasks, workflow tests, middleware and tools below (agents and MCP servers: `llms/kinds-agent-mcp.md`) share the envelope conventions (`guid?`, `description?`, `docs?`, `tags?`, `history?`) unless noted.
35
+ - `task({ name, guid?, description?, docs?, datasource?, active?, tags?, history?, schedule?, stack?, middleware? })` — a scheduled background job (function-like `stack`, no `input`/`response`). `active?` defaults `true`; `active: false` deploys it parked.
36
+ - `schedule?`: a `ScheduleDef[]` (NOT a single object) — `{ startsOn, freq?, repeatEnabled?, endsOn?, endsEnabled? }`. `startsOn`/`endsOn` are **timestamp strings** validated at encode time — `"2026-01-01T00:00:00Z"`, or the space-separated `"2026-01-01 00:00:00+0000"` a pulled workspace carries — never epoch numbers, and never zoneless; `freq` is the repeat interval **in SECONDS** (omitted ⇒ runs ONCE) — spell it `every("15m")`, a compile-time duration-to-seconds helper (`s`/`m`/`h`/`d`/`w`, terms concatenate). No cron, no timezone — a fixed-offset `startsOn` drifts an hour at each DST change; for a local time run hourly, gated on the local hour `withFilters(c.now(), fl.epochms_date("G", "America/New_York"))`. `repeatEnabled?`/`endsEnabled?` follow whether `freq`/`endsOn` is set and are recovery-only — state one to reproduce a stored gate left OFF. Fires on an ephemeral (see Gotchas).
37
+ - `workflowTest({ name, guid?, description?, docs?, datasource?, active?, tags?, stack? })` — an end-to-end test. NO `input`/`response`: `.call` something with an `as`, then assert on that var — `s.function.call({ fn, input, as: "r" })`, `s.expect.to_equal({ expr: ref("r"), value: c.int(42) })`. `s.expect.*` belongs here — it is not inert elsewhere (a failure 500s the request), so treat one in a query/function/task as a mistake to remove. `active?` defaults `true`; chain tests with `s.workflow_test.call({ workflowTest: <def handle> })`.
38
+ - `datasource?`: Default `""` is an EMPTY datasource (recommended), not "no datasource". Any non-empty name makes the engine CLONE it before EVERY run — against production-sized data, slow enough to fail the run, which is why `"live"` warns at compile time. On an EPHEMERAL, `"live"` holds only the seed fixtures (plus rows a `--keep-data` deploy kept), so it is the way to read `table({ seed })` rows — a small clone, dropped after the run. ⚠ The value is STORED, so clear it before you promote or the same test clones the real database.
39
+ - `middleware({ name, guid?, description?, docs?, resultStrategy?, exceptionPolicy?, tags?, history?, input?, stack?, response?, responseShape?, tests? })` — a pre/post interceptor (function-like `stack`); attach it via a host's `middleware: { pre, post }`. ⚠ `input` ENCODES but an ATTACHED middleware never has it bound — the host request binds its own inputs, so `inp()` inside pre/post fails at runtime with `Unable to locate input` and a declared default does not stand in (`export()` warns). Read the request body with `s.util.get_all_input` instead; it yields a `{ type, vars }` envelope whose `vars` DIFFERS BY PHASE: `pre` → the request inputs (`payload.vars.<field>`); `post` → `{ status, result }`, the host's outcome (`payload.vars.result.<field>`; plus `payload` on an error). A request field read in `post` 500s after the host already ran (`export()` warns); read the request there with `s.util.get_raw_input`. `s.middleware.call` is the one path that DOES bind the declared map.
40
+ - `resultStrategy?`: `"merge" | "replace"` (default `merge`) — how the middleware `response` folds into the accumulator of the phase it is ATTACHED to, and the two phases accumulate DIFFERENT things. In `post` the accumulator is the host's RESULT, so a returned object changes what the CALLER receives. In `pre` it is the host's REQUEST INPUTS, so a returned object changes what the HOST receives. `merge` folds key-by-key, `replace` substitutes wholesale; either way the next entry in the chain sees the updated value. So `pre` + `replace` DISCARDS every caller input the middleware does not re-emit, and a discarded input is then unreadable — a 500, even where it is declared `required: false`, because input defaulting has already happened by the time the override lands.
41
+ - `exceptionPolicy?`: `"silent" | "rethrow" | "critical"` (default `"rethrow"` — a throw ABORTS the request and surfaces the authored error/status, which is what a guard wants). `"silent"` swallows the throw and lets the request through, so a guard set to it is NOT enforced — use it only for advisory middleware. `"critical"` is `"rethrow"` plus skipping the `post` chain.
42
+ - `tool({ name, guid?, description?, instructions?, docs?, enabled?, title?, annotations?, icons?, output?, tags?, history?, input?, stack?, response?, responseShape?, middleware? })` — a function-like operation (`input`/`stack`/`response`) that a toolset (MCP server or agent) exposes. Register it, then reference it from a toolset's `tools`. `title`/`annotations`/`icons`/`output` are MCP metadata (`llms/kinds-agent-mcp.md`).
43
+ - An addon is a single table-bound db query, NOT a statement stack: `addon({ name, table, tableAlias?, where?, sort?, output: [cols], cardinality?: "single"|"list"|"count"|"exists"|"aggregate", group?, eval?, input?, context? })`, registered via `registerAddons([...])`.
44
+ - `table` auto-fills the `context.dbo` binding. ⚠ Never author `table: null` — that is a BROKEN table-less addon returning nothing; `codegen` emits it only for an already-broken pulled object.
45
+ - `tableAlias` is its SQL alias (`context.dbo.as`), qualifying `where`/`sort` columns (`col("merchant.id")`).
46
+ - `where`/`sort` take the same surface as `s.db.query`; `where` encodes `context.search`, `sort` encodes `context.return.list.sort` (`return.single.sort` with `cardinality: "single"`) and is refused with `count`/`exists`. `where` is the predicate binding the addon to the parent row — `expr(col("id"), "=", inp("user_id"))`.
47
+ - `cardinality` shapes the result (`context.return.type`, omitted for the `"list"` default). Rarer context (`eval`/`bind`/`lock`) stays raw `context` passthrough.
48
+ - End to end — define, register, attach:
49
+ ```ts
50
+ const author = addon({ name: "author", table: users, input: { user_id: input.int() }, where: expr(col("id"), "=", inp("user_id")), output: ["id", "display_name"], cardinality: "single" });
51
+ workspace("forum").registerAddons([author]); // plus the tables/queries
52
+ s.db.query({ table: threads, addon: [{ addon: author, as: "_author", input: { user_id: out("author_id") } }], as: "rows" }) // rows[]._author: { id, display_name }
53
+ ```
54
+ ### Responses
55
+
56
+ The `response?` field (on functions, queries, tools, middleware, and
57
+ response-bearing triggers) maps to the stored `result[]`:
58
+
59
+ - `ResponseDef = Value | Record<string, Value>`.
60
+ - A single `Value` → one unnamed result item: `response: ref("rows")`.
61
+ - A record → one named item per key: `response: { user: ref("u"), token: ref("t") }`.
62
+ - Omitted → empty `result[]` (no response body).
63
+
64
+ ### Expressions (`expr`)
65
+
66
+ `expr(left, op, right)` builds the comparison used by every condition/`where`
67
+ surface — `s.conditional`/`s.while` `when` (incl. each `elif` branch), and
68
+ `db.query` `where`/`additionalWhere` (and the search triggers) — one shared tree.
69
+
70
+ - `op`: `=`, `!=`, `>`, `<`, `>=`, `<=` (JS aliases `==` `===` `!==` are accepted and normalized).
71
+ - `left`/`right` are `Value`s — `col("x")` (a table column, `db.*` statements only), `ref`, `inp`, `auth(...)`, or `c.*`.
72
+ - For the full operator set (`in`/`like`/`ilike`/`between`/`contains`/`overlaps`/`@>`/`~`/`search`/…)
73
+ use `cmp(left, op, right, { ignoreEmpty? })`; compose nested boolean logic with `and(...)`/`or(...)`.
74
+ - ⚠ The wider `cmp` operators are DATABASE-only (`where`, table view filter, db trigger
75
+ `search`). A RUNTIME condition — `s.conditional`/`elif`, `s.while`, `s.precondition`,
76
+ `array.*` `if` — takes the `expr` set only; the rest are refused at build time because
77
+ deployed they fail the request with `Invalid op: <op>` on that branch, usually a guard.
78
+ `cond.*` builds the filter-then-compare form that DOES run there, and handles the operand
79
+ direction (the engine's `in` filter pipes the ARRAY, the reverse of `contains`): `cond.in`,
80
+ `notIn`, `contains`/`icontains`/`notContains`, `startsWith`/`endsWith` (+ `i` forms),
81
+ `empty`/`notEmpty`, `isNull`/`notNull`, `between(v, lo, hi)`, `has(obj, path)`, `count(v, n)`.
82
+ Each returns a `Comparison`, usable anywhere `expr(...)` is.
83
+ - A condition/`where` accepts a single `expr(...)`/`cmp(...)`, an `and()`/`or()` group, an array of
84
+ those (ANDed), or (for `where`) a raw `Value`. `s.conditional`/`s.while`/`s.switch`, `db.query`,
85
+ `precondition`, and the `array.*` predicates all take the same TREE shape (operators per above).
86
+ - ⚠ `mixed(a, { or: b }, { and: c })` reproduces a container whose terms do NOT all join the
87
+ same way — pulled workspaces contain it. **Do not author it** (export warns `condition.mixed`). The
88
+ stored form does not record the grouping, and the two places it can appear disagree: a
89
+ branch (`s.conditional`/`s.while`/`precondition`) folds terms strictly left to right, so
90
+ `a OR b AND c` is `(a OR b) AND c`, while a `db.query` filter applies the engine's
91
+ AND-before-OR precedence and selects `a OR (b AND c)`. Write `and(or(a, b), c)` or
92
+ `or(a, and(b, c))` — each says one reading in every context. Pulls report these as
93
+ `ambiguous-condition`.
94
+ - A **filtered** operand (`withFilters(...)`) works inline in any condition/`where` (conditional,
95
+ while, `db.query`/addon, …) — e.g. `cmp(withFilters(col("title"), fl.trim()), "=", inp("q"))`.
96
+ - **Compose a rule set as SIBLINGS, not a folded chain.** `and(...rules)` takes any
97
+ number of terms and encodes flat; `rules.reduce((acc, r) => and(acc, r))` nests one
98
+ container per rule, which costs quadratic bytes (512 terms: 394 KiB flat, 21 MiB
99
+ folded) and is refused past 128 levels. Mixed joins: `and(or(...anyOf), ...allOf)`.
100
+ - e.g. `db.query({ table: posts, where: expr(col("author"), "=", auth("id")), as: "rows" })`.