@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
@@ -0,0 +1,433 @@
1
+ # Object kinds
2
+
3
+ Every object kind Xano SDK can author, and how to split a workspace across microservices.
4
+
5
+ ## Object kinds
6
+
7
+ Every top-level Xano object is a registered kind with a factory and a `Xano.register*`
8
+ method: `defineFunction`, `table`, `query`, `apiGroup`, `tool`, `prompt`, `resource`,
9
+ `mcpServer`, `agent`,
10
+ `task`, `workflowTest`, `middleware`, `addon`, `realtimeServer`, `realtimeChannel`,
11
+ `realtimeMessage`, `knowledge`, `microservice` (its own section below), `workspaceConfig`,
12
+ and the seven trigger factories below. Signatures and payload keys are in
13
+ `llms/object-kinds.md` and `llms/triggers.md`; what follows is what
14
+ the types don't tell you.
15
+
16
+ **A knowledge item's body is a file, and `mode` is a running cost.** `knowledge()` is the
17
+ markdown a workspace's AI agents read before they act, and it is the one kind whose payload
18
+ is prose — so the body is named by path (`knowledgeFile("./runbook.md", import.meta.url)`)
19
+ rather than written as a string, and `refs: knowledgeDir(...)` ships a whole folder the
20
+ agent searches on demand — UTF-8 text files only (other content is refused), and a
21
+ symlink is not followed. What the agent actually receives is decided by `type` and `mode`,
22
+ and nothing in the types warns you: an `agents.md` item is injected in full on every turn
23
+ whatever `mode` says, `mode: "always"` spends the body's whole length on every request, and
24
+ the default `mode: "auto"` sends only the name and `description` until a request matches.
25
+ Write that `description` to be matched against a request rather than as a title. Full shape
26
+ in `llms/kinds-knowledge.md`.
27
+
28
+ **Triggers take a callback stack.** `stack: (t) => [...]` whenever the stack reads a trigger
29
+ input (a plain `stack: [...]`, as every other kind uses, is accepted when it reads none) —
30
+ because a trigger's inputs are **implied by its type** (fixed by Xano, not
31
+ editable) and injected automatically. So triggers take no `input` field, and the typed
32
+ handle `t` is the only way to read them (`response: (t) => ...` on response-bearing types).
33
+ The seven types are `tableTrigger`, `realtimeServerTrigger`, `realtimeChannelTrigger`,
34
+ `mcpServerTrigger`, `agentTrigger`, `workspaceTrigger`, and `errorTrigger`; they share one
35
+ stored envelope discriminated by `obj_type`.
36
+
37
+ ```ts
38
+ tableTrigger({
39
+ name: "on-user-insert",
40
+ table: users,
41
+ actions: { insert: true },
42
+ // Optional row filter, evaluated by the DATABASE before the stack runs — so it
43
+ // names the SQL pseudo-tables with col(), NOT the t handle. Rejected with
44
+ // `truncate`; insert cannot read OLD.*, delete cannot read NEW.*.
45
+ search: cmp(col("NEW.email"), "!=", c.text("")),
46
+ stack: (t) => [
47
+ // t.new("email") is typed to the row; t.action is the op; t.old is null (insert-only).
48
+ s.db.add({ table: auditLog, row: { email: t.new("email"), event: t.action } }),
49
+ ],
50
+ });
51
+ ```
52
+
53
+ **A workflow test is an end-to-end test, and its `datasource` decides what it can see.**
54
+ `workflowTest` takes no `input` and no `response` — it calls other objects and asserts on
55
+ what they bind. Leave `datasource` off by default: `""` runs against an **empty**
56
+ datasource. Naming one makes the engine **clone** that datasource before every run, so
57
+ pointing a test at production-sized data is slow enough to fail the run outright. `"live"`
58
+ warns at compile time; every other name is your call. On an **ephemeral**, `"live"` holds
59
+ only the seed fixtures, so it is how you read `table({ seed })` rows — a small clone,
60
+ dropped after the run. ⚠ The value is **stored on the object**, not a per-run flag: clear
61
+ it before you promote, or the same test clones the real database.
62
+
63
+ Empty means empty: **no `table({ seed })` rows exist while the test runs** by default, so
64
+ every `db` read misses unless the test specifies `datasource: "live"` (for seed data in
65
+ ephemerals) or creates what it needs first — typically a `defineFunction` fixture the stack
66
+ calls before anything else. A test written against a seeded row fails with your own
67
+ precondition message, which reads as a wrong id rather than an empty database. A failing
68
+ `s.api.call` is the other surprise: it **binds the error envelope** (`{code, message}`) to
69
+ its `as` and carries on rather than raising, so a later assertion gets blamed for a call
70
+ that failed several statements earlier — assert on `ref("r.code")` when a call may fail.
71
+ `llms/tests.md` carries the rest, including what `s.api.call` can and cannot do about
72
+ authentication.
73
+
74
+ ```ts
75
+ workflowTest({
76
+ name: "signup_works",
77
+ tags: ["smoke"],
78
+ // datasource omitted on purpose — "" is an EMPTY datasource, not "no datasource".
79
+ stack: [
80
+ s.function.call({ fn: createUser, input: { email: "a@b.c" }, as: "created" }),
81
+ s.expect.to_be_defined({ expr: ref("created") }),
82
+ s.expect.to_equal({ expr: ref("created.status"), value: c.text("ok") }),
83
+ // A regex assertion takes a PATTERN, so build it with `c.regex(...)`.
84
+ s.expect.to_match({ expr: ref("created.id"), value: c.regex("^usr_[a-z0-9]+$") }),
85
+ ],
86
+ });
87
+ ```
88
+
89
+ **Saved unit tests are a different thing, and they hang off the object.** A `query`,
90
+ `defineFunction` or `middleware` takes a `tests` array — the tests the Xano editor shows.
91
+ Each is a named set of inputs run against *that* object, asserted with the top-level
92
+ `expect.*` helpers. (`s.expect.*` — see [Authoring reference](authoring.md) — builds a
93
+ *statement* for a workflow-test stack;
94
+ `expect.*` builds a record stored on a test. They are not interchangeable, and the types
95
+ enforce it.) Any statement in the stack can return a **mock** instead of doing its work,
96
+ keyed by test name — and only while that named test runs, so a mock changes nothing about
97
+ a normal request.
98
+
99
+ A unit test's `datasource` works exactly as a workflow test's, with the same default:
100
+ `""` is an **empty** datasource, so **no `table({ seed })` rows are visible while a unit
101
+ test runs** either. Every `db` read misses, and an assertion on the first row fails against
102
+ a deployment whose endpoint returns those rows over HTTP a second later. The same
103
+ ephemeral-only `datasource: "live"` rule applies here, including clearing it before
104
+ promoting. Otherwise, create what the test needs inside the run — a `defineFunction`
105
+ fixture the stack calls first — or `mock` the read.
106
+
107
+ ```ts
108
+ query({
109
+ name: "score",
110
+ verb: "POST",
111
+ input: { score: input.int({ required: true }) },
112
+ tests: [
113
+ {
114
+ name: "adds one",
115
+ input: { score: c.int(1) },
116
+ // Subject first — argument order is the assertion.
117
+ expect: [expect.to_equal(resp(), c.int(2))],
118
+ },
119
+ ],
120
+ stack: [
121
+ // Returns 2 while "adds one" runs; does nothing on a real request. A name
122
+ // no test declares throws at compile time.
123
+ s.set_var("total", c.expression("$input.score + 1"), {
124
+ mock: { "adds one": c.int(2) },
125
+ }),
126
+ ],
127
+ response: ref("total"),
128
+ });
129
+ ```
130
+
131
+ A pull brings tests back, along with a query's saved request/response `example`. The one
132
+ thing it withholds is a test's auth `token` — that is an expiring credential rather than
133
+ authored configuration, so `xanosdk init --from` reports it as a deliberate omission instead of
134
+ writing it into a committed tree.
135
+
136
+ **Four of the `Run …` statements only run inside a workflow test.** `s.api.call`,
137
+ `s.task.call`, `s.trigger.call` and `s.workflow_test.call` are resolved by the engine at
138
+ run time, and outside a `workflowTest` stack it cannot reach the target — so one of them in
139
+ a query, function or task type-checks, exports, imports and **deploys clean**, then answers
140
+ the first real request
141
+ with `ERROR_FATAL: <Type> does not exist`. It is not per host kind: the same call fails
142
+ identically from a function that a query runs. `xanosdk export` refuses them outside a
143
+ workflow test. `s.function.call`, `s.tool.call`, `s.middleware.call` and `s.addon.call` run
144
+ from any stack, as does `s.function.run` — the ordinary way to invoke a function. To share
145
+ logic between two endpoints, put it in a `defineFunction` and `s.function.run` it from both.
146
+
147
+ **`s.expect.to_match` takes a regex PATTERN, not text.** The engine runs it through PHP
148
+ `preg_*`, which reads the first character as the delimiter — so a `c.text("^usr_.*$")` there
149
+ is a pattern the engine cannot run, and the assertion fails against the very string it was
150
+ written for. `c.regex("^usr_.*$")` (or `c.regex(/^usr_.*$/)`) wraps and escapes it; a bare
151
+ `c.text` pattern is refused at compile time and pointed here. A `ref`/`inp` pattern, whose
152
+ text isn't visible to the check, is passed through untouched.
153
+
154
+ **Realtime** — the only three-level containment chain in the SDK: `realtimeServer` owns
155
+ `realtimeChannel`s, which own `realtimeMessage` handlers (a message is the realtime
156
+ analogue of a query — its own typed payload and stack). Pass the **handle**, not a name: a
157
+ channel path is unique only within its server. A channel's `input` types its **path** params
158
+ (`rooms/{room_id}`); a message's `input` types the message **payload**. A server is off
159
+ until `enabled: true`.
160
+
161
+ ```ts
162
+ const chat = realtimeServer({ name: "chat", enabled: true });
163
+
164
+ const room = realtimeChannel({
165
+ name: "rooms/{room_id}", // `input` types the PATH params
166
+ server: chat,
167
+ input: { room_id: input.int() },
168
+ publish: { who: "authenticated" },
169
+ conversation: { enabled: true, limit: 50 }, // client-visible transcript
170
+ });
171
+
172
+ realtimeMessage({
173
+ name: "send", // `input` types the message PAYLOAD
174
+ channel: room, // the handle carries the server too
175
+ input: { body: input.text({ required: true }) },
176
+ deliverTo: "channel", // or "sender" (request/response) / "others"
177
+ stack: [s.debug.log({ value: inp("body") })],
178
+ });
179
+ ```
180
+
181
+ The client side is derived too, the same way `query().getPath()` works — `chat.getUrl(BASE)`
182
+ builds the socket URL (`wss://…/ws/<canonical>`, with a tenant base URL translated into the
183
+ socket's `/ws/<tenant>:<canonical>` form) and `room.getChannel({ room_id: 42 })` builds the
184
+ path a client joins. Both throw rather than guess. In a **browser bundle**, reach for the
185
+ generated manifest's `socketUrl`/`channelPath` instead — same addresses, same checks, without
186
+ importing the defs (see
187
+ [A type-safe frontend, for free](../README.md#a-type-safe-frontend-for-free)).
188
+
189
+ Five traps account for most realtime bugs. The full wire protocol — every server frame,
190
+ the presence roster shape, the at-least-once client contract — is in `llms/kinds-realtime.md`.
191
+
192
+ - **An empty return denies, and so does a crash.** `connect` and `join` are gates: return
193
+ `{ allowed: c.bool(true) }` or any truthy value to admit. A stack that falls through, or a gating
194
+ trigger with no `response`, refuses everyone — and a raise refuses too, because the gate is
195
+ seeded with a deny it keeps when the stack throws. Both failure modes lock the door, so the
196
+ risk to plan for is a self-inflicted lockout, not a breach: guard every drill inside a gate
197
+ with `ref(path, { safe: true })`, since `db.get` binds `null` on a miss. `export()` warns on
198
+ the missing `response`; nothing can warn about the raise. Gating is opt-in — a server with
199
+ no `connect` trigger admits everyone.
200
+ - **Only `null` drops a message.** In a `deliver` trigger (per recipient) and in a message
201
+ handler, `false`/`0`/`""` all deliver the message unchanged, and a crash broadcasts the
202
+ sender's original unvalidated payload. Return `null` to suppress. So a redaction check
203
+ written as a boolean sends the very message it was meant to hide. Per-viewer redaction
204
+ also takes **two objects**: the `deliver` trigger *and* `delivery: { perRecipient: true }`
205
+ on its channel. Either half alone delivers the payload unchanged to everyone, so a gate
206
+ whose return semantics are perfect still ships unredacted if the flag is missing — and the
207
+ flag costs a stack per recipient per message, so it is opt-in. `export()` warns on both
208
+ halves.
209
+ - **`conversation: { enabled: true }` alone stores nothing.** `limit` defaults to `0`, and
210
+ `0` means retain none. Always pass a `limit`. What a handler broadcasts *is* the stored
211
+ row, so broadcast everything a future joiner needs to render it.
212
+ - **An idle socket is reaped after ~10 minutes.** A listen-only client (feed, dashboard,
213
+ presence sidebar) must send `{ action: "ping" }` or any frame periodically, or it silently
214
+ drops and reconnects forever.
215
+ - **`s.realtime.publish` is the push direction, and it is fail-soft.** It bypasses the
216
+ channel's `publish.who` (authorization belongs in your stack), does not invoke the named
217
+ message's handler, and swallows a missing or disabled server — a mis-targeted publish is
218
+ silent. Pass the server handle and a filled-in path (`room.getChannel({ room_id: 42 })`),
219
+ never the template — a constant channel still carrying `{param}` throws at author time, and
220
+ a constant server or channel naming nothing this workspace registers warns at export.
221
+
222
+ **The superseded realtime layer.** Xano has had two realtime generations and they reuse the
223
+ same words. `realtimeTrigger(...)` and `s.api.realtime_event(...)` belong to the old
224
+ workspace-global layer; they are supported only so `codegen` can bring back a workspace that
225
+ holds them, and they are named in `llms/legacy.md` rather than in the authoring catalogs.
226
+ Aiming `s.api.realtime_event` at a current-layer channel publishes into the void — use
227
+ `s.realtime.publish({ server, channel, data })`, which names the owning server and so can
228
+ resolve the channel.
229
+
230
+ **MCP servers & agents** — both persist under the `toolset` payload key, so an `mcpServer`
231
+ and an `agent` **sharing a name collide**. A `tool({...})` is its own kind, referenced by
232
+ handle from either.
233
+
234
+ ```ts
235
+ // Auth is PER-TOOL and works like a query's: name an auth table({ auth: true }).
236
+ mcpServer({ name: "books", tools: [{ tool: searchTool, auth: users }] });
237
+
238
+ const assistant = agent({
239
+ name: "assistant",
240
+ llm: { type: "xano-free", systemPrompt: "Be helpful.", prompt: "Answer the question." },
241
+ // Pass the handles directly; the `{ tool, enabled?, auth? }` wrapper (above)
242
+ // is only for per-tool auth or `enabled: false`.
243
+ tools: [searchTool],
244
+ });
245
+
246
+ // Agents have NO public endpoint — invoke them in-stack from any host with a stack.
247
+ query({
248
+ name: "ask", verb: "POST", apiGroup: api,
249
+ input: { question: input.text({ required: true }) },
250
+ stack: [s.ai.agent.run({ agent: assistant, args: obj({ question: inp("question") }), as: "answer" })],
251
+ response: { text: ref("answer.result") },
252
+ });
253
+ ```
254
+
255
+ - **The run result is an envelope, not the completion.** The model's text is at **`.result`**
256
+ — `ref("answer")` is the whole metadata object (`finishReason`, `steps`, …). Both are
257
+ typed, so `InferResponse` reflects either.
258
+ - **`llm` is a provider-discriminated union** — `anthropic` / `openai` / `google-genai` /
259
+ `xano-free` (which needs no API key) — each with its provider's typed fields.
260
+ - **Structured output types the call site.** Author `output: { schema: { … } }` on the agent
261
+ with the `input.*` catalog and `.result` is typed from it wherever the handle is passed —
262
+ no second witness. The type-only `resultShape` is only for overriding that, or for an
263
+ agent referenced by bare name.
264
+ - **String settings are Twig-templated at run time.** The `args` you pass to
265
+ `s.ai.agent.run` become `{{ $args }}` (env vars are `{{ $env.NAME }}`), which is how an
266
+ endpoint's inputs reach the prompt. Numeric and boolean fields are not templated. Build a
267
+ dynamic arg with `obj({...})`, not `c.obj`.
268
+ - **`mcpServer().getUrl(HOST)`** derives the Streamable-HTTP endpoint from the def, the same
269
+ contract as `query.getPath()`. Resolve once — handing the result back in as a `HOST` throws
270
+ rather than append a second endpoint path. Agents expose only `getCanonical()`.
271
+
272
+ **MCP prompts and resources** are their own kinds (`registerPrompts` /
273
+ `registerResources`), exposed by listing them on an `mcpServer` — agents take tools only.
274
+ A tool, prompt or resource can also carry the metadata MCP clients show: a `title`,
275
+ `icons`, and on a tool, behavior `annotations` and an `output` schema.
276
+
277
+ ```ts
278
+ const summarize = prompt({
279
+ name: "summarize",
280
+ input: { post_id: input.int({ required: true }) },
281
+ stack: [s.db.get({ table: posts, fieldValue: inp("post_id"), as: "post" })],
282
+ response: ref("post.title"), // a string is one user message
283
+ });
284
+
285
+ const post = resource({
286
+ name: "post",
287
+ uri: "posts://{post_id}", // each {variable} is exactly one input field
288
+ mimeType: "application/json",
289
+ input: { post_id: input.int({ required: true }) },
290
+ stack: [s.db.get({ table: posts, fieldValue: inp("post_id"), as: "post" })],
291
+ response: ref("post"),
292
+ });
293
+
294
+ mcpServer({ name: "posts", tools: [searchTool], prompts: [summarize], resources: [post] });
295
+ ```
296
+
297
+ - **`s.mcp.elicit` re-runs the stack.** It asks the client's user mid-call; when they
298
+ answer, the whole stack runs again from the top. Ask first, then act — `export()` warns
299
+ when a write comes before an elicit. Clients older than MCP `2026-07-28` get
300
+ `{ action: "cancel" }`, so handle it. `s.mcp.progress` reports progress to a client that
301
+ asked for it.
302
+
303
+ **Background execution.** `s.function.run` and `s.ai.agent.run` take a `runtime` block
304
+ (`{ mode: "async-shared" }`, or `"async-dedicated"` with `cpu`/`memory`/`timeout`/`maxRetry`)
305
+ that moves the call off the request path. This is **not** a performance knob: Xano rewrites
306
+ an async call to a statement that dispatches and continues, so it does not return the
307
+ function's result — don't bind `as` expecting a value. Collect results later with
308
+ `s.await({ ids })`.
309
+
310
+ ## Microservices
311
+
312
+ A microservice is a container workload deployed alongside the workspace and called from a
313
+ stack with `s.microservice.request`. Two mutually exclusive shapes chosen by `kind`: `builtin`
314
+ declares containers (image/ports/resources/env/command/args) plus optional `ingresses`, and
315
+ `helm` points at a chart and its `values`; passing both throws.
316
+
317
+ ```ts
318
+ export const echo = microservice({
319
+ name: "echo",
320
+ deployment: {
321
+ replicas: 2,
322
+ containers: [{
323
+ name: "echo",
324
+ image: "ealen/echo-server:latest",
325
+ ports: [{ servicePort: "8080", containerPort: "80" }],
326
+ resources: { cpu: "50m", ram: "256Mi" },
327
+ }],
328
+ },
329
+ });
330
+ ```
331
+
332
+ Call it by passing the def itself. `port` folds into the single `"name:port"` host string
333
+ the engine reads, and is optional — a microservice exposing exactly one `servicePort`
334
+ resolves to it, and one exposing several requires it. A port the microservice doesn't expose
335
+ is a type error where the def's ports are known, and a build-time throw otherwise:
336
+
337
+ ```ts
338
+ s.microservice.request({ as: "res", host: echo, path: "/health" });
339
+ ```
340
+
341
+ Only `host` and `path` are required. `method`, `params`, `headers`, `timeout`, and
342
+ `follow_location` default to the engine's own values (`GET`, `{}`, `[]`, `10`, `true`) and are
343
+ always written — this statement's schema requires them, so they can't be left off the wire;
344
+ you just don't have to type them.
345
+
346
+ `host` binds by name, not by guid, because that is how the engine resolves it — so renaming
347
+ a microservice fixes every call site at once. A plain `"name:port"` string is also accepted
348
+ and is the only way to reach an instance-level microservice, which isn't a workspace object;
349
+ nothing checks that spelling, so prefer the def wherever there is one.
350
+
351
+ A container takes time to come up, so `xanosdk deploy` waits for it: after the import it
352
+ reads each microservice and reports whether it is ready, still starting, or failed, then
353
+ lists them. Skip the wait with `--skip-liveness`. The same report is available any time from
354
+ `xanosdk status`, `xanosdk ephemeral get <name>`, and `xanosdk workspace details`.
355
+
356
+ Two outcomes, and only one of them is a warning:
357
+
358
+ - **The engine reports the microservice broken** (an image that won't pull, a container that
359
+ won't start) — the deploy **exits 4**. Waiting longer cannot change that answer, and a URL
360
+ and a ✓ printed over a dead workload is not a successful deploy. This is the default; there
361
+ is no flag to turn it off.
362
+ - **It simply hasn't reported ready by the end of the wait** — a warning, exit `0`. The
363
+ backend is live and a slow container usually follows moments later. Pass
364
+ **`--require-microservices`** to make that exit 4 too, which is what CI wants: nobody is
365
+ there to find out whether "should come up shortly" happened.
366
+
367
+ Exit 4 is the microservice sibling of exit 3 (a `--static` upload that failed while the
368
+ backend deploy stood): the import committed, and something it deployed is not serving. The
369
+ URL and the JSON summary still print either way — the exit code is what carries the
370
+ difference.
371
+
372
+ `tenantDeploy: "manual"` rows are reported but never waited on — nothing starts them for you.
373
+ Reach for it when the row should exist without a workload behind it; `examples/sandbox` uses
374
+ it so deploying the examples doesn't wait on containers.
375
+
376
+ Container names are free-form: they need not match the microservice's own name, and nothing
377
+ about addressing depends on them. A stack reaches the **microservice** name (plus a
378
+ `servicePort`), whichever containers sit behind it, so a multi-container workload names each
379
+ one for what it is.
380
+
381
+ **This surface is early and expected to change**, and every export of a workspace declaring a
382
+ microservice prints a notice saying so — the docs are read before writing, which is not where
383
+ you are when it matters. `configs` and `volumes` are typed and `@deprecated` but **not
384
+ deployable**: the engine rejects an import carrying either, so `export()` fails the build
385
+ rather than letting the deploy fatal minutes in, after provisioning has begun. Declare a value
386
+ the workload reads as a container `env` entry, and storage as a container `volumes` entry
387
+ (`emptyDir`, `persistent`, or `config`). Both fields stay typed so a pulled workspace holding
388
+ one still decodes.
389
+
390
+ Two fields carry secrets into the bundle — and into a pulled tree — verbatim:
391
+ `chart.values` and `registryAuth.dockerconfigjson`, because otherwise a pulled microservice
392
+ could not be redeployed.
393
+
394
+ **What "out of band" can and cannot mean here.** Reading `process.env` in a def is never it —
395
+ that resolves at EXPORT time, writing the literal into the bundle and into git with it:
396
+
397
+ ```ts
398
+ // WRONG, for either field.
399
+ microservice({ name: "app", registryAuth: { dockerconfigjson: process.env.REGISTRY_JSON! } });
400
+ ```
401
+
402
+ What the engine *does* resolve at deploy time is a **reference** to a workspace environment
403
+ variable (`workspaceConfig({ env })`). Two spellings, because a container env entry is a
404
+ structured slot and a chart value can sit at any path:
405
+
406
+ ```ts
407
+ // A builtin container: name the variable, and the secret stays out of the bundle.
408
+ env: [{ name: "API_KEY", fromEnv: "STRIPE_SECRET" }]
409
+
410
+ // A helm chart: the same reference, for a value at any depth.
411
+ chart: { values: 'auth:\n token: "${env.STRIPE_SECRET}"\n' }
412
+ ```
413
+
414
+ Only the variable *name* travels in the bundle, rotating the workspace variable takes effect
415
+ on the next deploy with no edit here, and a reference the workspace does not define **fails
416
+ the deploy** rather than resolving blank. `value` and `fromEnv` are mutually exclusive on one
417
+ entry; setting both is a build error.
418
+
419
+ `registryAuth.dockerconfigjson` has **no** such form — it is not scanned for references — so
420
+ for that field there are two honest options, both about where the bytes live rather than about
421
+ hiding them:
422
+
423
+ 1. **Leave `registryAuth` unset** — a public image, or a pull credential attached to the
424
+ microservice outside this workspace. Nothing then carries a credential.
425
+ 2. **Accept that the tree is secret-bearing** — keep `workspace.json` and any pulled tree out
426
+ of git, or rotate the credential once it lands there.
427
+
428
+ Export prints a notice naming every microservice whose bundle bytes carry a literal (chart
429
+ values built entirely of `${env.NAME}` references carry nothing and are not reported), so
430
+ this can't happen quietly; `--strict` does **not** promote it, since shipping a
431
+ private-registry workload is a legitimate end state. When what you actually need is a secret
432
+ your *stack* reads, the mapped surface is `workspaceConfig({ env })` + `env("NAME")` — see
433
+ [Middleware, request history and env vars](authoring.md#middleware-request-history-and-env-vars).
@@ -0,0 +1,43 @@
1
+ # Project structure
2
+
3
+ How a Xano SDK project is laid out on disk, and why registration is explicit.
4
+
5
+ Lay objects out however you like and register them explicitly — there's no folder
6
+ auto-discovery magic (deliberately):
7
+
8
+ ```
9
+ xano/
10
+ ├── function/ get_user.ts export const getUser = defineFunction({...})
11
+ ├── table/ table.ts export const user = table({...})
12
+ │ └── trigger/ on_insert.ts export const onInsert = tableTrigger({...})
13
+ ├── query/ public.ts export const publicApi = apiGroup({...})
14
+ │ public/posts_GET.ts export const posts = query({...})
15
+ ├── agent/ assistant.ts export const assistant = agent({...})
16
+ ├── realtime_server/ chat.ts export const chat = realtimeServer({...})
17
+ │ chat/room.ts export const room = realtimeChannel({...})
18
+ │ chat/room/send.ts export const send = realtimeMessage({...})
19
+ ├── workspace.ts export const workspaceSettings = workspaceConfig({...})
20
+ └── index.ts workspace("my-app").registerTables([...]).registerFunctions([...])…
21
+ ```
22
+
23
+ Objects nest under whatever owns them. Anything with children — an API group, a
24
+ realtime server, a channel — is a file named for itself sitting *beside* the folder
25
+ holding its children, so `chat.ts` opens in a tab you can tell apart and a group with
26
+ no queries needs no folder at all. Realtime is the deepest, being the only three-level
27
+ hierarchy in a workspace — server, then channel, then message — and a trigger sits in
28
+ a `trigger/` folder at whichever level it fires on.
29
+
30
+ Paths are lower case throughout — an HTTP verb is the one exception, because it is
31
+ the method rather than a word. Bindings keep the object's own casing, so a file name
32
+ and the symbol it exports can differ.
33
+
34
+ That is the shape `xanosdk init --from` writes, and its `index.ts` re-exports every object
35
+ by name — import from the tree's root rather than from a file, since a file path moves
36
+ when an object's parent or its `_shared.ts` placement changes. Hand-authored projects are
37
+ free to use any other layout; only `index.ts` registering the objects matters.
38
+
39
+ `workspace("my-app")` is the natural entry point — sugar for
40
+ `new Xano().registerWorkspace({ name: "my-app" })`, returning the same chainable registry.
41
+ Authoring is **declarative def-objects** passed to factories; there is no callback/chaining
42
+ builder. `xano.export()` returns the importable `packageExport` bundle, and
43
+ `xanosdk export`/`deploy` read the module's default export.