The long tail of app-development - load that skill first for the core workflow (write a function, wire gipity.yaml, deploy, test). This reference covers the function runtime's module system in depth, plus operational details you reach for on demand.
ES module syntax - what you can write
The runtime compiles your code as a real ES module (since 2026-05-29). Any valid single-file ESM parses cleanly:
// All of this works in one file:
import { z } from './schemas.js'; // relative imports (multi-file only - see below)
export const VERSION = 3; // top-level const, exported or not
export function validate(input) { ... } // named exports alongside the default
const cfg = await loadConfig(); // top-level await
export default async function handler(ctx, { db }) { ... }
What does NOT work and the exact error you'll see:
- Bare imports (
import _ from 'lodash') → "Bare import 'lodash' is not allowed in function code. Use a relative path like './helpers.js'." Nonode_modules, no built-in Node modules (fs,path,crypto- for secrets use the injectedrandomToken(chars)service). - No default export → "Function must `export default` a callable handler."
- Default export isn't a function → "Function 'export default' must be a function (got object)."
- Genuine parse error → "Function file 'index.js' did not parse:
" - the file name in the message tells you which file the error is in (helpful in multi-file functions).
Multi-file functions
When a function grows past a single file - testable helpers, shared constants, etc. - turn it into a directory:
functions/
song-render/
index.js # entry - must have `export default async function ...`
sanitize.js # helper - `export function sanitizeRenderOptions(...)`
constants.js # `export const DEFAULT_MODE = 'preview'`
index.js is the entry; sibling files are reached via relative import:
// functions/song-render/index.js
import { sanitizeRenderOptions } from './sanitize.js';
export default async function songRender(ctx, { db, jobs, guid }) {
const opts = sanitizeRenderOptions(ctx.body?.render_options);
// ...
}
Why use multi-file:
- Testable helpers. Unit-test directly from
tests/*.test.jswithimport { x } from '../functions/song-render/sanitize.js'. Single-file helpers can only be exercised through end-to-end function calls. - Cleaner separation. Pure helpers stay pure.
Helpers don't see services. Only the entry module receives ctx, db, fetch, secrets, env, console, jobs, guid, and the app services (llm, image, tts, sound, music, video, transcribe, location). Inside a helper, those names are undefined. If a helper needs the DB, the entry passes it as an argument:
// functions/song-render/sanitize.js - PURE helper, no service access
export function sanitizeRenderOptions(raw) { /* validates raw, returns clean obj */ }
// functions/song-render/persist.js - needs db, so accept it as arg
export async function saveRender(db, songGuid, opts) {
await db.query('INSERT INTO renders ...', [songGuid, opts]);
}
// functions/song-render/index.js - entry passes db down
import { sanitizeRenderOptions } from './sanitize.js';
import { saveRender } from './persist.js';
export default async function (ctx, { db }) {
const opts = sanitizeRenderOptions(ctx.body);
await saveRender(db, ctx.body.song_guid, opts);
}
Import rules and errors:
- Imports must be relative -
./helper.jsor./utils/foo.js(bare specifiers rejected as above) - Imports can't escape the function directory -
../../etc/foo.jsfails with "Import '../../etc/foo.js' escapes the function directory." The one sanctioned exception is the shared../_lib/prefix (next section). - A missing sibling fails clearly: "Cannot resolve import './nope.js' → 'nope.js' - file not in function payload. Available: index.js, sanitize.js"
- Circular imports work (a → b → a) - ESM cycles resolve via the module loader's link cache
- The entry filename is always
index.js
Limits: up to 20 files per function (shared _lib files count toward this), 256 KB total source, max 4 levels of nesting.
Pick the shape that fits: single functions/foo.js for simple handlers; functions/foo/ directory once you have a helper you'd like to unit-test.
Shared modules across functions (functions/_lib/)
When two or more functions need the same code - a schema registry, validation, SQL builders - put it in functions/_lib/ instead of copy-pasting it into each function directory (copies drift):
functions/
_lib/
registry.js # shared - `export function describeObject(...)`
record-read/
index.js # imports it: `import { describeObject } from '../_lib/registry.js'`
record-write.js # single-file functions can import it the same way
At deploy, _lib/ files are bundled into every function's payload, so editing a shared module re-deploys all functions automatically. The rules:
- Import as
../_lib/<file>.jsfrom any function file (works from single-file functions too) _libfiles may import each other (./fmt.js) but can't import function files - shared code stays function-agnostic_libfiles are pure like helpers: nodb/ctx/services; the entry passes them as arguments_libis reserved: a function named_libor a_lib/subdirectory inside a function directory is a deploy error- Unit-test shared modules from
tests/*.test.jsviaimport { x } from '../functions/_lib/registry.js'
The function runtime supports single-file ESM (shipped 2026-05-29), multi-file functions with relative ./helper.js imports (2026-05-30), and shared _lib modules imported as ../_lib/<file>.js (2026-06-09).
Kits that ship backend pieces (gipity add records …)
Some kits install more than frontend code. A kit whose manifest declares
functions: true / migrations: true copies its functions/ and
migrations/ trees into the app's own functions/ and migrations/ (the
records and agent-api kits are the prototypes - generic registry-driven
CRUD with an audit event spine, and named API keys for agent writes). Rules:
- Kit-owned files are sealed. Functions,
_libmodules, and migrations a kit installs are upgraded by re-runninggipity add <kit>at a newer version, which overwrites them. Never edit them in place; put app code in your own function/migration files. - Kit migrations order themselves by filename (
000-kit-*.sqlsorts before your001-*.sql). If the app has nosqldeploy phase, the install adds one. - The kit's
gipity.yamlfunction entries declare only the kit's own core tables. When you register your own objects/tables, append them to the kit functions'tables:lists yourself - e.g. the records kit'srecord-read/record-writeneed your object tables listed before they can serve them. Your edits survive re-install (entries are merged by name, never clobbered). - Kits can require other kits (
requires: ["records"]) -adderrors with the missing prerequisite's name.
Full guide to the records + views kits (registry-driven CRUD, field
types, event spine, generated tables/kanban/forms) → the
app-records skill.
Versioning & Rollback
Every update creates an immutable version:
fn_manage rollback --name my-function --version 3
Management Commands
fn_manage list # List all functions
fn_manage logs --name X # Execution logs
fn_manage delete --name X # Delete
fn_manage rollback --name X --version N # Rollback
Rate Limits
- 300 requests per 5-minute window (per IP) — waived during
gipity test(all test runs share one sandbox IP, so the platform exempts a project's own test run; see "What else is isolated" above) - 500 rows / 128 KB per query result
Related Skills
app-development- the core guide: function shape,ctx,gipity.yamlpermissions, and the deploy/test loopapp-database- the per-app Postgres database: migrations, thedbhelper, transactions, table permissionsapp-testing- testing deployed functions:ctx.fn.call/callAs, the isolated test DB, fixturesapp-records- the records + views kits: registry-driven CRUD, field types, event spinedeploy- the deploy pipeline andgipity.yamlmanifest