@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.txt ADDED
@@ -0,0 +1,493 @@
1
+ # xanosdk v1.0.0
2
+
3
+ > TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.
4
+
5
+ Xano is a hosted backend platform: one WORKSPACE serves a managed PostgreSQL
6
+ database, HTTP API endpoints, background tasks, realtime websocket channels, AI
7
+ agents and MCP servers, file storage, and redis. This SDK authors that workspace in
8
+ TypeScript — typed def objects registered on a `workspace(name)` — compiled to one
9
+ importable bundle by `xanosdk export ./xano/index.ts`, shipped live by `xanosdk deploy`.
10
+
11
+ Def modules execute at BUILD time only: `s.*` factories return data, and the engine
12
+ runs the compiled stack per request — statements in order, a statement's `as:`
13
+ naming a runtime variable, `response` the HTTP body. Every dynamic operand is a
14
+ TAGGED value — `ref("x")` a stack variable, `inp("x")` an input, `auth("id")` the
15
+ caller, `c.*` a constant — resolved at request time; JS operators over them do not
16
+ compute (see Gotchas). Requests share no memory — state persists in tables or redis.
17
+
18
+ Authorable: statement surfaces 218/218, filters 246 (246 typed).
19
+ Not authorable here: branch, market_item, realtime_channel, run.job, run.service, tablemap — these cannot be authored and do not survive a pull; reasons in `coverage.objectKinds.unmodeled`.
20
+
21
+ This file is the whole always-read surface: the mental model, the deploy contract,
22
+ every cross-cutting gotcha, and control flow. Per-surface detail lives in the topic files listed
23
+ below — open the one whose condition matches the task, skip the rest. For
24
+ exhaustive per-entry detail in NEITHER — a statement's field schema with engine
25
+ defaults, a filter's full argument list, the `storedName` mapping — do a TARGETED
26
+ lookup in the shipped `manifest.json` (a program imports it as
27
+ `@xano/sdk/manifest.json`, needing `with { type: "json" }` in Node ESM;
28
+ it is ~70k tokens, so never read it whole). Its top-level keys are `name`, `version`, `description`, `coverage`, `values`, `objectKinds`, `fieldTypes`, `statements`, `filters`, `cli`, `cliGlobalFlags`. `statements` and `filters` are ARRAYS, not maps — SELECT, do not index:
29
+ jq '.statements[] | select(.sPath=="db.get")' manifest.json
30
+ jq '.filters[] | select(.name=="json_decode")' manifest.json
31
+ ⚠ `fields` is null on the 65 `declarative: false` statements (`db.get`, …): typed wrappers whose arguments are the factory's `.d.ts` signature. Null ≠ missing.
32
+ Select a statement on `sPath` (the `s.*` path you write), NOT `surface` (the XanoScript term): 24 of 218 differ — `var`→`set_var`, `break`→`foreach_break`, `foreach.remove`→`foreach_remove`, and every `expect.*`.
33
+
34
+ ## Topic files
35
+
36
+ Paths are relative to this file (`node_modules/@xano/sdk/` once installed), so a
37
+ plain file read resolves them at the version you have.
38
+ `llms-full.txt` is everything concatenated — one fetch for a reader that cannot open files.
39
+
40
+ - [Object kinds](llms/object-kinds.md): Read when deciding WHAT to build — every authorable primitive, what each is, and which factory + register method builds it. Also when `register*` will not typecheck an array built by `.flatMap()`/`.concat()`.
41
+ - [Core def shapes](llms/kinds-core.md): 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.
42
+ - [Consuming defs from a client](llms/client.md): Read when a frontend imports a def (`getPath()`/`InferResponse`) or calls the deployed backend — what the import costs, `window.XANO_HOST`, local dev, file URLs, and a Node spot-check.
43
+ - [Toolchain modules](llms/toolchain.md): Read when `package.json` has a `"xanosdk"` block or you run `xanosdk marketplace`.
44
+ - [Error index](llms/errors.md): 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.
45
+ - [Saved unit tests, assertions, and mocks](llms/tests.md): Read when authoring a `workflowTest()` stack, when a query/function/middleware carries `tests`, when a statement needs a `mock`, or when running a deployed environment's tests.
46
+ - [Agent and MCP def shapes](llms/kinds-agent-mcp.md): Read when defining an `agent()`, `mcpServer()`, `prompt()` or `resource()`.
47
+ - [Knowledge def shape](llms/kinds-knowledge.md): Read when the workspace defines a `knowledge()` item for its AI agents.
48
+ - [Realtime def shapes](llms/kinds-realtime.md): Read when the workspace defines a `realtimeServer()`, `realtimeChannel()`, or `realtimeMessage()` handler.
49
+ - [Triggers](llms/triggers.md): Read when authoring any trigger. A trigger's `stack` is a callback rather than the plain array every other kind takes, so the shape does not carry over.
50
+ - [Array and database statements](llms/statements-data.md): Read when the stack reads or writes rows (`s.db.*`), or transforms an array in place (`s.array.map`, `s.array.union`).
51
+ - [Statement runtime behavior](llms/statements-runtime.md): Read when you need to know what a statement's `as:` output holds, or why a bound variable is not the shape expected.
52
+ - [Auth, cross-object calls, and microservices](llms/statements-calls.md): Read when the stack authenticates or verifies a webhook (recipes here), calls another object (`s.function.run`, `s.api.request`, `s.tool.call`), sends email (`s.util.send_email`), or reaches a microservice.
53
+ - [Value catalog](llms/values.md): Read when you need a literal, a reference, or a tag you have not used before — `c.*`, `ref`, `inp`, `auth`, `col`, and what each one encodes to.
54
+ - [Column and input types](llms/fields.md): 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`.
55
+ - [Filter catalog](llms/filters.md): Read when piping a value through `fl.*` — the full catalog with each filter's argument types.
56
+ - [Lambda bodies (JavaScript)](llms/lambda.md): Read when writing a JavaScript body, or weighing whether to reach for one at all — `lam.fn`, `fl.lambda`, `fl.reduce`, `s.lambda`; each surface binds a different set of identifiers.
57
+ - [Lock file](llms/lock.md): Read when a `xano.lock` exists or should — renaming/pruning/importing identities, seeding the lock programmatically, or asking which commands write it.
58
+ - [Legacy paradigms and retired statements](llms/legacy.md): Read when the code was PULLED from an existing Xano instance — how a codegen'd tree reads, the shapes in it that must not be "fixed", and any `raw({ name: "mvp:…" })`, `realtimeTrigger()`, or name the catalogs do not list.
59
+ - [Statement catalog](llms/statements-catalog.md): Read for the field signature of a specific statement — every surface, grouped by `s.*` namespace.
60
+
61
+ ## Quickstart
62
+
63
+ Authoring is **declarative def-objects** passed to factories — there is no
64
+ callback/chaining builder.
65
+
66
+ ```ts
67
+ import { workspace, apiGroup, query, table, input, f, ref, inp, auth, s } from "@xano/sdk";
68
+
69
+ const users = table({
70
+ name: "users",
71
+ auth: true, // backs authentication
72
+ // `id` (int PK) + `created_at` (epochms) are auto-injected — don't declare them.
73
+ schema: {
74
+ email: f.email({ required: true }),
75
+ name: f.text(),
76
+ },
77
+ // Indexes: { type, fields: [{ name, op? }] }. `"unique"` is shorthand for `"btree|unique"`.
78
+ // A DOTTED name indexes a key inside a column (`xdo.email`); only the part before the
79
+ // first dot is checked against the schema.
80
+ index: [{ type: "unique", fields: [{ name: "email" }] }], // also: btree, hash, gin, search, gist, vector
81
+ });
82
+
83
+ const posts = table({
84
+ name: "posts",
85
+ schema: {
86
+ author: f.tableRef(users), // foreign key → users (NOT `ref`)
87
+ body: f.text({ required: true }),
88
+ },
89
+ });
90
+
91
+ const api = apiGroup({ name: "blog", canonical: "blog" }); // canonical → the URL token
92
+
93
+ const createPost = query({
94
+ name: "create_post", verb: "POST", apiGroup: api, auth: users, // the auth table
95
+ input: { body: input.text({ required: true }) },
96
+ stack: [
97
+ s.db.add({ table: posts, row: { author: auth("id"), body: inp("body") }, as: "post" }),
98
+ ],
99
+ response: ref("post"),
100
+ });
101
+
102
+ const listPosts = query({
103
+ name: "list_posts", verb: "GET", apiGroup: api,
104
+ stack: [s.db.query({ table: posts, sort: [{ sortBy: "created_at", dir: "desc" }], as: "rows" })],
105
+ response: ref("rows"),
106
+ });
107
+
108
+ export default workspace("my-blog")
109
+ .registerTables([users, posts])
110
+ .registerApiGroups([api])
111
+ .registerQueries([createPost, listPosts]);
112
+ ```
113
+
114
+ Compile: `xanosdk export ./index.ts --out bundle.json` (or `writeBundle(app, path)`
115
+ from `@xano/sdk/node` in code). The default export must be the `Xano` registry. The entry must be an
116
+ ES module (Xano SDK defs are ESM-only): set `"type": "module"` in the nearest
117
+ package.json or name the entry `.mts`. `npm init -y` writes `"type": "commonjs"`,
118
+ which fails with a "must be ES modules" error until you switch it to module.
119
+
120
+ Set `canonical` on every `apiGroup`. The engine mints the URL token server-side, so
121
+ without one a group's client paths are unresolvable until a lock exists: the bundle
122
+ exports fine and `xanosdk routes` / `getPath()` then fail on the very queries it just
123
+ built. An explicit `canonical` resolves them from the source alone.
124
+ ⚠ A canonical is unique across the whole INSTANCE, not per workspace — two
125
+ workspaces cannot serve the same one, and no flag shares it. One set in code is a PIN:
126
+ a merge refuses (exit 2) when another workspace holds it. One the lock minted is a
127
+ preference the instance may keep or replace with a random token, and `--static` is
128
+ refused when it does.
129
+
130
+ Build warnings: `export()` prints the shapes that deploy clean and then do the wrong
131
+ thing (a `bulk.update` zero-filling omitted columns, an `ignoreEmpty` on an empty
132
+ operand, a `ref()` nothing binds). Each has a legitimate use, so each only warns —
133
+ but nothing fails on a message no one reads, so in CI and in unattended agent builds
134
+ pass `--strict` (`emitBundle(app, { strict: true })` / `app.export({ strict: true })`):
135
+ every warning becomes a hard failure. Same bundle bytes either way.
136
+ Before calling the work done: `npm run typecheck`, `xanosdk export <entry> --strict`,
137
+ `xanosdk deploy <entry>`, then `xanosdk test run-all` against what deployed.
138
+
139
+ Identity: object guids derive from `(type, name)` — a query's from `(api group,
140
+ verb, name)` — so renames change identity.
141
+ EVERY build freezes every guid + api-group/toolset canonical in a lock file
142
+ BESIDE THE ENTRY FILE — `xano/xano.lock` for the standard scaffold,
143
+ NOT the project root — which you COMMIT (`--no-lock` opts out; CI guard
144
+ `--frozen-lock` fails instead of changing it).
145
+ To rename an object: rename in code, export (stderr prints the exact fix-up), run
146
+ `xanosdk lock rename <kind> <old> <new>`, export again — the original guid is emitted
147
+ under the new name, so the engine renames in place instead of delete+create. Taking
148
+ over an existing workspace: `xanosdk lock import <live-bundle.json>` first, then
149
+ export. Pruning, programmatic seeding, and which commands write the lock:
150
+ `llms/lock.md`.
151
+
152
+ ## Deploy
153
+
154
+ `xanosdk init` → `xanosdk deploy` → URL. A deploy runs the same compile pipeline as
155
+ `export` (honoring `xano.lock`), imports the result, and prints the URL.
156
+ ⚠ Every deploy is a FULL REPLACE of its destination — objects AND records.
157
+
158
+ **Backends** — one grammar: `workspace` | `ephemeral[:<name>]` | `local-engine[:<name>]`
159
+ (no credential) | `tenant:<name>` | `release:<name>` | a bundle path where a file fits. Reads
160
+ `--from`, writes `--to`, `test --on`; positional on `deploy`/`pull`/`generate`/`tables`/
161
+ `impersonate`. Omitted = the ephemeral or local engine this project last deployed to, never
162
+ workspace/tenant; bare `deploy` compiles the project onto an ephemeral instead (`--local-engine`:
163
+ an engine on this machine), and `generate`/`init --from` need one. A refusal, typed or tracked,
164
+ names the reason and the spellings (`publish`, `release create` refuse a local engine).
165
+ `release create --from` records a run: no bundle path, no `release:`,
166
+ and a `tenant:` only if throwaway (`standard`/`run` refused).
167
+
168
+ **Destinations** — ephemeral by default, real only when you say so:
169
+
170
+ - `xanosdk deploy [source]` → a NAMED, auto-expiring ephemeral (~1h; `--expires-hours`
171
+ 1–24), tracked in `./.xano/ephemeral.json`, so deploying again REFRESHES it at the same
172
+ URL; if it expired or was swept, a fresh one is created and the new URL called out.
173
+ ⚠ Only the BACKEND URL survives a refresh — the replace clears static hosting, so
174
+ `--static` publishes a NEW host every run and the previous URL stops serving
175
+ (not under `--keep-data`, which MERGES an env a deploy filled: rows kept, no re-seed,
176
+ removed objects deleted; ⚠ a rename loses values, a RETYPE reads unfit ones as null, env
177
+ VALUES are not applied, a table trigger refuses it — `--reset` replaces and re-seeds).
178
+ - `xanosdk release create <name>` cuts from a running source and LANDS in your workspace
179
+ — the record that it came up, and the only thing `promote`/`tenant deploy` take. A
180
+ taken name is refused, and so is one that addresses a LOCATION instead of naming: `/`,
181
+ `\`, `:`, or a leading `.`. Every export derives its FILENAME from the name it is given,
182
+ so `release|ephemeral|workspace export` refuse those same shapes unless `--path` names
183
+ the file. Carries EVERY table's SCHEMA either way; `--seed[=<guids>]` adds
184
+ ROWS — guids from `xanosdk tables <backend>`; a redeploy KEEPS them.
185
+ - `xanosdk release transfer <name> --to-profile <profile>` copies a release to another
186
+ workspace/instance by CONTENT (tar sha256): identical → reused; same name, other
187
+ bytes → exit 2.
188
+ - `xanosdk promote <release>` / `xanosdk tenant deploy <tenant> <release>` land it.
189
+ `promote` adds a branch; ⚠ its TABLE edits hit live
190
+ (`--allow-shared-schema-changes`). `tenant deploy` REPLACES: DROPS tables it lacks,
191
+ CONVERTS retypes (`--seed`: rows).
192
+ `promote` refuses a taken name/pinned slug (`SDK_IDENTITY_CONFLICT`), then
193
+ EXITS NON-ZERO if a declared object did not arrive or a pinned slug is not served;
194
+ `--set-live` waits on that (previous branch serving). Extra objects: reported;
195
+ a check missing an input reports `unverified`, exit 0. `--json` on every release write:
196
+ `completed` yes|no|unknown; UNKNOWN exits 9 — run `resolveWith`, never retry blind.
197
+ `--expect-live <label>` (with `--set-live`) refuses if live moved; not atomic. Inspect a staged branch with
198
+ `workspace export --branch <label>` or `workspace diff <entry> --branch <label>`.
199
+ ⚠ The redis family, `s.lambda`, `s.db.direct_query`, `s.await`, `s.microservice.request`,
200
+ `s.zip.*`, the raw-file readers, Datadog and Webflow need an INSTANCE CAPABILITY: they
201
+ deploy clean and fail on the first request if the target lacks it. `export`/`deploy` list
202
+ what the bundle needs — compare it to the target yourself.
203
+ - Backend SECRETS stay out of the repo: declare the NAME, value empty, in `workspaceConfig({ env })`
204
+ passed to `registerWorkspace()`; the VALUE lives in `xano/.env` — gitignored, kept across a
205
+ pull, read by every build. `--backend-env-file <path>` replaces that default (CI has
206
+ no `xano/.env`), `--env-var KEY=VALUE` beats both, and `xanosdk env pull` fills the file from
207
+ a running backend — the only command that writes `xano/.env`.
208
+ ⚠ A deploy REPLACES the env set: a declared name with NO value refuses the deploy rather
209
+ than clearing the live one — `--allow-empty-env=NAME` clears it on purpose.
210
+ ONE live value, no deploy: `xanosdk env set NAME` (value on stdin) / `env unset NAME`.
211
+ (`--static-env` is the other one — PUBLIC frontend config.)
212
+ - A DOCUMENTATION token is a secret too, in a SECOND ignored file: source declares only the
213
+ gate — `documentation: { require_token: true }` on `workspaceConfig` or an `apiGroup` — and
214
+ the value lives in `xano/.secrets.json`, keyed by the object it gates. A literal `token`
215
+ FAILS the export. `xanosdk pull` writes that file and every build reads it back;
216
+ `xanosdk secrets fill` MINTS one per gate that has none — a gate you AUTHORED has no value
217
+ to pull. CI, which does not have the file, passes `--secrets-file <path>` or
218
+ `--doc-token "<scope>=<value>"`
219
+ (scope = `workspace`, or the group's NAME).
220
+ ⚠ OMISSION IS OPPOSITE PER SCOPE: off `workspaceConfig` the target's gate is left alone;
221
+ off an `apiGroup` it is CLEARED (an absent key there is written as the engine default). An
222
+ unsupplied gate refuses a `deploy` and fails the export for a publishing group.
223
+ - `xanosdk deploy --to workspace|tenant:<name>` skips the release flow to merge a LOCAL
224
+ build in, keeping the checks that need one (`--dry-run`, `--prune`, loss warnings) and
225
+ leaving nothing to roll back to. Prefer the release path.
226
+ ⚠ Branches scope LOGIC: tables and microservices are shared by EVERY branch, so a
227
+ schema change reaches production on any branch — refused without
228
+ `--allow-shared-schema-changes` — and dropping a column destroys its data.
229
+ ⚠ `--replace` CLEARS BY WORKSPACE, NOT BY BRANCH: every non-live branch goes, with the
230
+ saved versions that would restore one — unrecoverable. Refused without
231
+ `--allow-branch-deletion`; `--yes` does not imply it. The default merge leaves them alone.
232
+ - `xanosdk workspace reset-tables <entry> --table <guid>` puts NAMED tables back to their
233
+ compiled `table({ seed })` rows, touching no other table. Dry run unless `--write`.
234
+ ⚠ ROWS BEFORE LOGIC: rows are workspace-shared like schema, so they reach production when
235
+ applied even while the logic reading them is staged — no ordering makes a half-data change
236
+ atomic. Reset rows FIRST, then `promote`: new rows under old logic break only a feature
237
+ being removed; new logic over old rows breaks the one you are shipping.
238
+ - `xanosdk pull` refreshes THIS project from a backend: rewrites decoded files,
239
+ keeps yours, lists and confirms (`--yes`).
240
+ - `xanosdk generate <source>`: the decoded tree ONLY, no project.
241
+ - `xanosdk status` names the env this project last deployed to, its URL and its expiry.
242
+
243
+ **Frontend wiring.** `--static <dir>` (or `xanosdk publish <dir>`) injects the env's backend URL as
244
+ `window.XANO_HOST` into EVERY html document in the build, before the app bundle runs,
245
+ so the frontend needs no rebuild to target an env. How to read it, local dev, verifying
246
+ the inject, and displaying a stored file: `llms/client.md`.
247
+ ⚠ A static host serves its files verbatim, so everything injected is PUBLIC — base
248
+ URLs and publishable keys only, never secrets. Secrets go in backend env, read
249
+ server-side via `env(name)`.
250
+ ⚠ `xanosdk preflight` uses the deploy credential unless `XANO_VALIDATE_INSTANCE` /
251
+ `XANO_VALIDATE_TOKEN` (+ optional `XANO_VALIDATE_WORKSPACE_ID`) are set.
252
+
253
+ **Full CLI surface:** `xanosdk <command> --help` — every command, flag, default and accepted
254
+ backend; `manifest.json` carries the same in its `cli` array.
255
+ **Style:** reach statements through the `s` namespace (`s.db.add`, `s.math.add`, …).
256
+ The flat aliases (`dbAdd`, `setVar`, …) are identical in output; prefer `s.*`.
257
+
258
+ ## Gotchas
259
+
260
+ Non-obvious authoring rules:
261
+
262
+ ### Values and references
263
+
264
+ - **No callback builder.** Flat def-objects + `register*`, not
265
+ `workspace(w => w.table(...))`. `workspace(name)` returns a named `new Xano()`;
266
+ tables are `table({ schema: { col: f.text() } })`.
267
+ - **Reference-helper picker:** `ref` = stack var (`as:` output), `inp` = input,
268
+ `col` = table column (in `db.query` `where`), `auth("id")` = the caller,
269
+ `c.*` = a constant. Pick by what you're pointing at.
270
+ - **Foreign key is `f.tableRef(table)`, not `ref`.** `ref(name)` references a
271
+ stack variable (a value); `f.tableRef` is the column constructor.
272
+ - **Tagged values are DATA — a JS template literal cannot compose them.** `${ref(...)}`
273
+ (or `"a" + ref(...)`) stringifies the tag object at BUILD time: ``c.text(`Hi ${ref("u.name")}`)``
274
+ type-checks and encodes the literal text `Hi [object Object]`, served verbatim.
275
+ `export()` warns; `--strict` fails. Compose at RUNTIME:
276
+ `withFilters(c.text("Hi "), fl.concat(ref("u.name")))`, an `obj({...})`/record member,
277
+ or `c.expression('"Hi, " ~ $var.u.name')`.
278
+ - **Declare inputs with `input.<type>()`, read them with `inp("name")`.** `inp()`
279
+ resolves ONLY against this def's own `input` block — a value produced earlier in
280
+ the stack is `ref("var.field")`, not `inp("field")`. An undeclared name fails at
281
+ runtime with `Unable to locate input` (`export()` warns).
282
+ - **`get_input`/`get_raw_input` read the whole payload**, not one named input
283
+ (args are `{ as?, encoding?, excludeMiddleware? }` — no `name`). For a single
284
+ input use `inp("name")`.
285
+ - **To match every row, omit `where` — never pass a constant.** `where: c.bool(true)` is
286
+ not "no filter": a bare scalar literal states no condition and the engine reads the
287
+ resulting `context.search` as garbage, so it throws. An absent `where` is how "every
288
+ row" is spelled. A raw `Value` `where` stays the escape hatch for a clause built
289
+ elsewhere — `inp("clause")`, `ref("built_where")`, or `c.text("id > 0")`.
290
+
291
+ ### Tables and columns
292
+
293
+ - **System columns are auto-injected.** `id` + `created_at` are prepended to
294
+ every table (`system: true` by default); declaring them by hand is redundant.
295
+ `id` is an `int` PK by default; pass `idType: "uuid"` on the table for a uuid key.
296
+ Both are valid targets wherever a column name is accepted — `db.query` `sort`/
297
+ `output`, a `db.get`/`edit`/`del` `fieldName`, etc. (the column-name type is
298
+ `keyof schema | "id" | "created_at"`), and both appear in `InferRow<typeof table>`.
299
+ - **A column named `run` is reserved.** The table deploys and reads back fine, then
300
+ EVERY `s.db.add` into it 400s — at any column type, with or without a value — and
301
+ the error names the column while complaining about the VALUE. Use `run_id`. Exact,
302
+ case-sensitive, one name: `Run`/`runs`/`run_id` are fine. `--strict` fails on it.
303
+ - **An input named `run` is reserved when a call passes it**: the callee gets
304
+ defaults or a 400. HTTP binds it fine. `--strict` fails on it.
305
+ - **Numeric columns lose money quietly.** A normalized table's `f.decimal` reads
306
+ `0.12345678` back as `0.12346`; `f.int` saturates at `9223372036854775807`
307
+ instead of failing. Both at HTTP 200 and plausible on read, so it surfaces as
308
+ arithmetic that stops reconciling. Hold money as a count of the smallest unit;
309
+ exact bounds: `llms/fields.md`.
310
+ - **Self-referencing tables** need the bare-name form: inside `tweets`'s own
311
+ schema, write `f.tableRef("tweets", { type: "int" })` — the `const tweets`
312
+ handle isn't assigned yet, so the handle form throws "used before declaration".
313
+ - **Seed a table's starting rows with `table({ seed })`.** Rows typed against the
314
+ schema, inline (`seed: [{ name: "…" }]`) or from a file (`seedFile("./seed.json",
315
+ import.meta.url)`). Only `deploy` ships them, and a re-deploy re-seeds cleanly. Never
316
+ put secrets in `seed`. Row shape, `id` pinning, and the file rules: `llms/fields.md`.
317
+ - **`f.password()` defaults to `access: "internal"`, so `db.get` does NOT return it.**
318
+ A login stack that reads `ref("u.password")` after a plain `db.get` fails at runtime
319
+ with `Unable to locate var: u.password` — the column is simply absent from the row.
320
+ Name it in the read's `output` to pull it: `s.db.get({ table: users, fieldName: "email",
321
+ fieldValue: inp("email"), output: ["id", "email", "password"], as: "u" })`, then
322
+ `s.security.check_password`. `output` OVERRIDES column visibility — it is the only way to
323
+ read an `internal` column, and `export()` warns when a stack reads one a `db.get` did not
324
+ return.
325
+ - **Don't take a password through `input.password` on login — it double-hashes.**
326
+ An `f.password()` column hashes on write, and `input.password` *also* hashes the
327
+ submission on bind, so `s.security.check_password` compares two different hashes
328
+ and a correct password always fails (`ok:false` on a found row). Take the submitted
329
+ password as `input.text()` on both signup and login and pass the plaintext straight
330
+ to `check_password` (which does the comparison hash itself). `export()` warns.
331
+
332
+ ### Reading and writing rows
333
+
334
+ - **DB reads are field-match, not `where`-expr.** `db.get`/`db.edit`/`db.del`/
335
+ `db.has`/`db.patch` match one field: `{ fieldName, fieldValue }` (`fieldName`
336
+ defaults to the PK `id`). Only `db.query` takes a `where`/`additionalWhere`
337
+ `expr(...)`; writes (`db.add`/`db.edit`/`db.add_or_edit`) take a `row`/`data`.
338
+ Signatures: `llms/statements-data.md`.
339
+ - **Single-field only — no composite match.** These ops match exactly ONE field;
340
+ there is no two-field form (the engine's by-field lookup takes a single
341
+ predicate). For a `(a, b)` existence/fetch — e.g. dedupe a `(habit, date)`
342
+ check-in — use `db.query({ where: [expr(col("habit"), "=", ...), expr(col("date"), "=", ...)], as })`
343
+ (a `where` array is ANDed) and branch on the result, rather than pushing the
344
+ check to the client.
345
+ - **Drilling into a maybe-null `db.get` result 500s — use `ref(path, { safe: true })`.**
346
+ `db.get` binds `null` on a no-match, but a nested `ref("owner.user_id")` raises a runtime
347
+ "Unable to locate var" (HTTP 500) when `owner` is null — an ownership guard throws instead
348
+ of failing cleanly. Two correct shapes:
349
+ - Guard existence first — `s.precondition({ expr: expr(ref("owner"), "!=", c.null()),
350
+ error_type: "notfound", error: c.text("Not found.") })` — then drill WITHOUT `safe`.
351
+ - Or drill null-safe inside an EXPRESSION or `obj()` operand:
352
+ `expr(ref("owner.user_id", { safe: true }), "=", auth("id"))` yields `null` (the guard
353
+ reads `false`) instead of 500ing. Works inside `obj({...})` too — no `s.set_var` hoist needed.
354
+ ⚠ EXPRESSION and `obj()` operands ONLY — never a `db.*` match argument. `null` is not a
355
+ legal `fieldValue`/`id`, so a chained get (fetch the child row, then its parent to check
356
+ the owner) fails with HTTP 400 `Missing param: field_value` one statement BEFORE the guard.
357
+ There guarding existence first is MANDATORY. `export()` warns on a safe ref in that position.
358
+ ⚠ That is for a base that EXISTS and may be null. If the base binds nothing at all (a typo),
359
+ `{ safe: true }` buries the bug as a silent `null` — `export()` warns with a did-you-mean;
360
+ fix the name instead.
361
+ - **`output` selects from what the statement BINDS, and a PAGED `db.query` binds the
362
+ envelope.** With `metadata: true` (the DEFAULT), the roots are `items.<column>` plus the
363
+ counters you want kept: `output: ["itemsReceived", "curPage", "items.id", "items.title"]`.
364
+ A bare column list there matches no envelope key, so ALL of them are dropped and the
365
+ endpoint answers `[]` at HTTP 200 with no error — and a `response` reading
366
+ `ref("rows.items")` then dies with `Unable to locate var: rows.items`. Prefix the columns,
367
+ or set `metadata: false` inside `paging` to select the rows directly.
368
+ `export()` warns; `--strict` fails.
369
+
370
+ ### Stacks and calls
371
+
372
+ - **Block specials nest a `body`, not a `stack`.** `s.for`/`s.foreach`/
373
+ `s.while`/`s.switch`/`s.try_catch`/`s.db.transaction`/`s.expect.to_throw`
374
+ take their sub-stack as `body` (`try`/`catch`/`finally` for `try_catch`);
375
+ `s.group(body)` and `s.util.post_process(body)` take it **positionally**.
376
+ `s.for` is **count-bounded** (`{ as, count: <Value>, body }`), not from/to. See the
377
+ authored signatures in `llms/statements-data.md`.
378
+ - **`s.api.call` / `s.task.call` / `s.trigger.call` / `s.workflow_test.call` are
379
+ WORKFLOW-TEST ONLY.** Outside a `workflowTest({...})` stack the engine cannot reach
380
+ the target, so one in a query/function/task deploys clean and then answers the first
381
+ real request with `ERROR_FATAL: <Type> does not exist: <type>:<n>` — and not per
382
+ host kind, the same call fails identically from a function a query runs.
383
+ `export()` refuses them. `s.function.call`, `s.tool.call`, `s.middleware.call` and
384
+ `s.addon.call` run anywhere, as does `s.function.run` (the ordinary function
385
+ invocation). To share logic between endpoints, put it in a `defineFunction` and
386
+ `s.function.run` it from both.
387
+ - **`expect.*` is a unit-test assertion; `s.expect.*` is a workflow-test statement.**
388
+ A statement's `mock` is keyed by TEST NAME; an undeclared name throws. See `llms/tests.md`.
389
+ - **A helper returning `Statement[]` widens the stack and kills `InferResponse`.**
390
+ Spreading `...requireX()` where the helper is typed `Statement[]` drops the stack's TUPLE
391
+ type, so EVERY `ref()`/`as` in that stack — even ones after the spread — resolves to
392
+ `unknown` and the response types as `StackTupleWidened`. Nothing fails at the helper; it
393
+ surfaces where the response is consumed, often a frontend typecheck. Fix: return
394
+ `statements(s.a(...), s.b(...))` — its tuple survives the spread. A helper that builds
395
+ its array in a LOOP cannot be a tuple; declare `responseShape` on the query there.
396
+ - **Build regex-filter patterns with `c.regex(body, flags?)`, never `c.text`.**
397
+ The regex filters (`regex_test`/`regex_match`/`regex_replace`/…) are PHP `preg_*`: the
398
+ piped value is the PATTERN and must be delimiter-wrapped, and the ARGUMENT is the subject.
399
+ Correct: `withFilters(c.regex(/^[a-z-]+$/i), fl.regex_test(inp("slug")))`.
400
+ - A bare `c.text("^…$")` is an invalid pattern that matches *nothing* for every input,
401
+ so a precondition on it silently rejects all values. `c.regex(body, "i")` wraps +
402
+ escapes it for you (a JS `RegExp` too: `c.regex(/^…$/i)`).
403
+ - Reversed — subject piped, pattern in the argument — reads correctly, type-checks, and
404
+ answers false for EVERY input, so an `if (matches) reject` guard admits what it refuses.
405
+ - Build time refuses both: `withFilters` throws on a bare `c.text` pattern from ANY position
406
+ in the chain (a normalizer in front of the regex filter is refused too; nothing upstream
407
+ adds the delimiters) and on a pattern found in the subject slot; `s.expect.to_match` is the
408
+ same PATTERN slot, refused both ways; a `ref`/`inp` pattern is passed through untouched.
409
+ `export()` warns on a reversed pair in stored bytes.
410
+ - Better still: a native typed input (`input.email`) over hand-rolled validation.
411
+ - **An `auth()`-keyed limiter FAILS the request on any host with no caller identity —**
412
+ **at every tier.** `s.redis.ratelimit({ key: withFilters(c.text("rl:"), fl.concat(auth("id"))), error: c.text("…") })`
413
+ is the per-user form, and a public query, a task, or a function has no caller identity.
414
+ It does not degrade to one shared bucket: on a public endpoint the FIRST call 403s,
415
+ under `max`, and the host never runs. Key off `sys.remoteIp()` there. Attaching it
416
+ at `apiGroup({ middleware })` or the workspace tier inherits the same failure onto
417
+ every member endpoint. Export warns, naming the tier.
418
+
419
+ ### Identity, build, and deploy
420
+
421
+ - **Same-name siblings collide — queries excepted: their identity carries group + verb.**
422
+ Guids derive from `(type, name)`, so two functions (or tables, toolsets, …) sharing
423
+ a name derive ONE guid and `export()` throws — give them DISTINCT names. A QUERY
424
+ derives from `(api group, verb, name)`, the engine's own uniqueness: `GET items` +
425
+ `POST items`, and `items` across two groups, coexist, and the lock keys the same
426
+ composed identity (`query:blog|GET|items`) — changing a query's verb or group
427
+ changes its identity. An explicit `guid` on a colliding pair clears the throw but
428
+ is NOT a lasting fix: a lock entry cannot hold two guids, so `export --lock`
429
+ refuses the pair (`export()` warns even unlocked); it is for pinning identity
430
+ across a rename, not for sharing a name.
431
+ - **`export()` vs `emitBundle()` vs `writeBundle()`:** `export()` returns the object,
432
+ `emitBundle()` the JSON, `writeBundle(app, path)` writes `xanosdk export`'s file. All
433
+ three run the SAME build-time checks, INCLUDING seed validation of a literal `seed: [...]`
434
+ array (row shape, unknown column, coercion, enum membership, the all-or-nothing `id`
435
+ rule). A DEFERRED seed — a thunk (`() => import("./seed.json")`) or `seedFile()` — is
436
+ checked only by the `xanosdk export`/`deploy` CLI path. Only `deploy` ships seed rows.
437
+ - Entry points: the `node:fs` writers (`writeBundle`/`writeArtifact`) and lock-file I/O
438
+ import from `@xano/sdk/node`, NOT the browser-safe `@xano/sdk` entry (a frontend
439
+ imports defs from it for `getPath()`/`InferInput`). `@xano/sdk/internal` is the
440
+ compiler machinery, never needed to author. READING a bundle back is `@xano/sdk/bundle`
441
+ — a statement walker (`2.if.0` paths), a structural hash, `mvp:*` catalog, `tableRefOf`.
442
+ - **Intra-workspace imports use `.js` specifiers** (`../tables/links.js`), not
443
+ extensionless — the defs compile under `moduleResolution: bundler`. Add the `.js`.
444
+ - **Agents authenticate with env vars — never `xanosdk login`.** `login` blocks on a
445
+ browser consent no agent can complete. Set all three of `$XANO_INSTANCE_URL`,
446
+ `$XANO_WORKSPACE_ID`, `$XANO_META_TOKEN` and run `deploy`/`promote` directly: no disk, no
447
+ rotation, so it survives repeated runs.
448
+ - The triple outranks every other credential (`--config`, `$XANO_PROFILE` and
449
+ `$XANO_REFRESH_TOKEN` included, each named on stderr when displaced). Setting SOME of the
450
+ three is a hard error, so a misspelled secret cannot fall through to whatever credential
451
+ the runner has. An explicit `--profile` against it is a hard error too, not a displacement.
452
+ - On disk: `xanosdk profile add <name> --instance … --workspace-id <n>` reads the token
453
+ from piped stdin, else PROMPTS (a flag would reach the process list); the instance must accept it.
454
+ `auth.json` holds a map of named profiles, selected by `--profile <name>`. The older
455
+ `$XANO_REFRESH_TOKEN` + `$XANO_CLIENT_ID` pair still works but ROTATES: single-use.
456
+ - **Event-driven objects fire on an EPHEMERAL.** A `task` (scheduled), an `mcpServer`,
457
+ and every trigger — `tableTrigger` included — run normally on an ephemeral env, which
458
+ is where `deploy` sends them. So test an event-driven design (screen-on-insert, cron
459
+ cleanup, MCP tool call) by deploying it and letting it run.
460
+
461
+ ## Statements
462
+
463
+ Reachable through the `s` namespace: `s.<path>({...})`. Declarative statements
464
+ take one typed args object (field names match the engine); every surface's field
465
+ signature is in `llms/statements-catalog.md`. Specials (`[special]`) are
466
+ hand-authored: control flow is below, and the rest are in the `llms/statements-*`
467
+ topic files this doc's navigation names.
468
+ Wrap an `input`-routed `value` field in `ignored(...)` to store it but SKIP it at
469
+ runtime — the engine records `<name>:ignore` and the parameter falls back to its
470
+ default. Not the same as an empty value, and not the same as omitting the field
471
+ (which stores no entry at all). Mostly seen on a pulled workspace. A field written
472
+ into `context` instead (the `s.math.*`/`s.text.append` mutation family) has no slot
473
+ for the flag and THROWS rather than dropping it.
474
+ Fields marked `value` take a `Value` (`c.*`/`ref`/`inp`); `comparison` takes an
475
+ `expr(...)`. A `→ as: <type>` suffix names what the statement's `as:` output var
476
+ holds (curated, not exhaustive — absence means read the `[output]` flag and prose).
477
+
478
+ ### Specials — authored signatures
479
+
480
+ Control flow & blocks (each nests a sub-stack; block specials name it `body`):
481
+
482
+ - `s.set_var(name, value)` · `s.update_var(name, value)` · `s.return(value)` · `s.comment(text)` — positional.
483
+ - **Every** statement takes `disabled?`/`description?` — annotations on the stack item, not args: `disabled: true` is Xano's "disable step" (kept in the stack, skipped at runtime), `description` the note beside it. Inline on object-arg factories; a trailing object on the positional ones (`s.set_var("x", v, { disabled: true })`).
484
+ - **Statements with an `as`** also take `asFilters?` — `fl.*` filters on the RESULT as it binds, in order, same slot as `disabled`: `s.set_var("x", v, { asFilters: [fl.trim(), fl.lower()] })`. Saves a follow-up `set_var`. Throws without an `as`. The bound variable is RETYPED by the chain (`db.query` + `[fl.count()]` → `number`); filters whose result the engine declares as `any` (`get`, `set`, `json_decode`, …) fold to `unknown`.
485
+ - `s.conditional({ when, then, elif?, else? })` — if/elif/else. `when` is a condition (`expr`/`cmp`/`and`/`or`); `elif` is an ordered `[{ when, then }]` (each an else-if branch); `then`/`else` are `Statement[]`.
486
+ - `s.for({ as, count, body })` — **count-bounded** loop (`as` is the index), NOT from/to; `count` is a `Value`, not a bare number.
487
+ - `s.foreach({ as, list, body })` — iterate `list`; `as` is the current item.
488
+ - `s.while({ when, body })` — `when` is a condition (`expr`/`cmp`/`and`/`or`).
489
+ - `s.switch({ on, cases: [{ when, body, break? }], default? })` — multi-way branch on a subject `Value` `on`; each `case`'s `when` is a literal `Value` matched against `on` (NOT a comparison — use `s.conditional` for `<`/`>`/ranges). ⚠ **Omitting `break: true` FALLS THROUGH** — the matched case also runs every LATER case body. Type-checks clean; only `export --strict` catches it.
490
+ - `s.try_catch({ try, catch?, finally? })` — three `Statement[]` blocks.
491
+ - `s.group(body)` / `s.util.post_process(body)` — take a `Statement[]` **positionally**.
492
+ - `s.foreach_break()` / `s.foreach_continue()` / `s.foreach_remove()` — nullary loop control.
493
+ - `s.expect.to_throw({ body, exception? })` — `body` is the statements expected to raise; `exception` is a `Value`, not a bare string.