inquir.yaml is the one build file for functions. It has the same shape for Node.js, Python and Go, and the browser editor, inquir deploy and Git deploys all read it the same way. It declares the OS packages a function needs (build.system) and its language packages (build.dependencies). A deploy reads the file; there are no separate console settings, CLI flags or API fields for either.
Where the file lives#
Put inquir.yaml at the root of the function's files: the function's folder in the editor's file tree, or the function directory you deploy with the CLI or from Git. It ships with the code, so every surface reads the same file. inquir.yml works too; inquir.yaml wins when both exist. The file may be up to 64 KiB.
A project can also keep one inquir.yaml at its root for all of its functions. When you deploy an SDK function that lives in a subdirectory (inquir deploy --asset <name>), its archive does not contain the root file, so the CLI (the release after 0.4.1) sends that file alongside the archive and the platform reads it as the project file. A Git deploy reads only the file in the directory it deploys.
Container applications use the same file name: version and services are the application manifest (see Container applications). Functions read only build and functions.
A complete example#
The build block at the top applies to every function deployed from this directory or project. A functions.<name> entry replaces it for one function. A function has one runtime, so write each dependency list in that runtime's syntax.
# inquir.yaml: the same file for Node.js, Python and Go functions build: # defaults for every function deployed from here system: presets: [ffmpeg] functions: thumbnails: # replaces the defaults for this function (no merge) build: system: presets: [imagemagick, fonts] packages: [poppler-utils] # Debian names; pin with name=version dependencies: [sharp@0.33.5] # Node.js: name@version ocr: build: system: presets: - name: tesseract langs: [deu, rus] dependencies: ["pytesseract==0.3.13"] # Python: a pip requirement ids: build: system: null # no system packages for this function dependencies: [github.com/google/uuid@v1.6.0] # Go: module@version
Schema reference#
Every key is optional. Unknown keys are refused at the top level, in build, in build.system and in functions.<name>: a typo such as build.sytem fails the deploy with a “did you mean” hint instead of silently dropping your packages. The browser editor completes and checks the file as you type.
| Key | Type | What it does |
|---|---|---|
build | map | Build settings for every function deployed from this directory or project. |
build.system | map or null | OS packages installed into the function image (Debian bookworm). null or an empty block declares “no packages”. |
build.system.presets | list, up to 8 | Platform presets by name. Only tesseract takes options: { name: tesseract, langs: [deu, rus] }. |
build.system.packages | list, up to 10 | Extra Debian packages by apt name, optionally pinned as name=version. |
build.dependencies | list, up to 200 | Language packages, read with the function runtime's rules: name@version for Node.js, a pip requirement for Python, module@version for Go. Each entry is at most 512 characters. |
functions.<name>.build | map | The same build block for one function, keyed by the function's name. It replaces the file's build key by key; lists are never merged. |
version, services | — | The container application manifest. Functions do not read these keys. |
System packages: build.system#
A deploy turns the block into the function's image: the runtime's slim Debian bookworm base plus one layer with your packages, installed by apt from Debian's official bookworm repositories. The image is keyed by its content, so a package set is built once and shared by every function that asks for the same set, in any workspace, and a deploy that only changes code never rebuilds it.
Packages are installed at build time only. Functions run as an unprivileged user and cannot apt-get install at runtime. Removing the block, or the file, removes the packages on the next deploy.
Node.js and Python functions can use system packages. Go system packages are coming soon: until then a deploy refuses build.system for a Go function and says so. The file and your code will not need to change when Go support lands.
Presets#
A preset is a curated, versioned set of Debian packages plus the environment variables its tools expect. The variables are baked into the image; a function environment variable with the same name overrides them.
| Preset | Installs | Environment |
|---|---|---|
ffmpeg | ffmpeg: ffmpeg and ffprobe (FFmpeg 5.1) | FFMPEG_PATH=/usr/bin/ffmpegFFPROBE_PATH=/usr/bin/ffprobe |
imagemagick | imagemagick (ImageMagick 6: convert, identify), ghostscript, fonts-dejavu-core. Debian's ImageMagick policy keeps PDF and PostScript disabled. | — |
libvips | libvips-tools, libvips42: the vips command and the library for pyvips (Node's sharp ships its own libvips) | — |
poppler | poppler-utils: pdftotext, pdftoppm, pdfinfo | — |
tesseract | tesseract-ocr 5 and tesseract-ocr-eng, plus tesseract-ocr-<lang> for each entry in langs | TESSDATA_PREFIX=/usr/share/tesseract-ocr/5/tessdata |
ghostscript | ghostscript (gs), the PostScript and PDF interpreter | — |
fonts | fonts-dejavu-core, fonts-liberation, fonts-noto-core, fonts-noto-cjk, fonts-noto-color-emoji, for headless rendering | — |
langs takes up to 8 of these codes: ara, bul, ces, chi-sim, chi-tra, dan, deu, ell, eng, est, fin, fra, heb, hin, hrv, hun, ind, ita, jpn, kat, kaz, kor, lav, lit, nld, nor, pol, por, ron, rus, slk, slv, spa, srp, swe, tha, tur, ukr, vie. inquir functions system presets prints the current catalog.
Debian packages: rules and limits#
- Use bookworm package names exactly as
aptknows them: lowercase letters, digits and+ . -, starting and ending with a letter or digit, with no... apt patterns and removals cannot be expressed this way; for a C++ compiler listbuild-essential, notg++. Each name must be a real package: the build checks every name withapt-cachebefore installing anything, and a virtual package (one that only other packages provide) fails it with a message that tells you to list a package that provides it. - Pin a version with apt's syntax,
name=version(for exampletesseract-ocr-deu=5.3.0-2). If Debian no longer ships a pinned version, the image build fails; update or drop the pin. - At most 10 packages per function (presets do not count toward the limit) and at most 8 presets. List each one once.
- Daemons, privilege tools, kernels and package tooling are refused:
systemd*,init,sudo,sudo-ldap,doas,openssh-server,docker*,containerd*,podman*,runc,crun,linux-image-*,linux-headers-*,*-dkms,apt-transport-*,apt,dpkg,cron,anacron,rsyslog,udev,dbus. The build also checks that exactly the requested packages were installed and that no privilege tool, SSH server, container engine or kernel package (sudo,doas,openssh-server,docker*,containerd*,podman*,runc,crun,linux-image-*,linux-headers-*,*-dkms) came in as a dependency, and it removes setuid and setgid bits from the image. - The added layer may be up to 1 GB compressed, and an image build may run for up to 15 minutes.
- A workspace can request at most 10 new package sets in 24 hours. A set the platform already has, because any workspace asked for it before, does not count.
- Custom build steps (
system.run) are not available.
Headless Chromium for Puppeteer or Playwright#
There is no browser preset: Chromium is a package list, and apt brings in its libraries. Launch /usr/bin/chromium with --no-sandbox (functions run unprivileged), use puppeteer-core or playwright-core so that no browser is downloaded, and give the function at least 1 GB of memory. If a library reads PUPPETEER_EXECUTABLE_PATH, set it to /usr/bin/chromium as a function environment variable. Turn on Network for the function to load external pages.
# inquir.yaml build: system: packages: [chromium, fonts-liberation, fonts-noto-color-emoji] dependencies: [puppeteer-core@^23.0.0] # no browser download; or list it in package.json
const puppeteer = require('puppeteer-core'); // Give the function at least 1 GB of memory and turn on Network. exports.handler = async (event) => { const browser = await puppeteer.launch({ executablePath: '/usr/bin/chromium', args: ['--no-sandbox', '--disable-dev-shm-usage'], }); try { const page = await browser.newPage(); await page.goto(event.url || 'https://example.com', { waitUntil: 'networkidle2' }); const pdf = await page.pdf({ format: 'A4' }); return { statusCode: 200, body: JSON.stringify({ pdfBase64: Buffer.from(pdf).toString('base64') }) }; } finally { await browser.close(); } };
Native modules need -dev packages#
When a function has system packages, its dependency install (npm ci, npm install, pip install) runs on the function's own image, so native addons and source distributions compile against the libraries you installed. Presets ship runtime libraries only, so add the headers yourself: pycairo needs packages: [libcairo2-dev, pkg-config], and psycopg2 needs libpq-dev. A Node addon that compiles with node-gyp also needs build-essential and python3.
Language packages: build.dependencies#
One list, with the same key for every runtime. A function reads it with its runtime's rules:
- Node.js:
name@version, an npm package name with a version, range or dist-tag (sharp@0.33.5,zod@^3.23.0). Git, file, URL and alias specs belong inpackage.json. - Python: a pip requirement with a version specifier (
requests==2.32.3,uvicorn[standard]>=0.30). Environment markers and direct URL references belong inrequirements.txt. - Go:
module@versionwith an exact version or pseudo-version (github.com/google/uuid@v1.6.0). Modules at v2 and later need the/vNsuffix in the path.
The deploy merges the list into the build copy of your source, never into your own files: the editor keeps showing exactly what you wrote. Node.js entries go into the dependencies of package.json, Python entries into requirements.txt, and Go entries into a require block in go.mod; each file is created when it is missing. Then the platform installs them, in handler and web mode alike: npm ci --omit=dev with a lockfile or npm install --omit=dev without one; pip install into .python_packages; for Go, go mod tidy and go mod vendor, after which the handler compiles offline from vendor/. A Node.js handler ships the whole production node_modules next to its bundle and keeps every installed package out of the bundle, so native addons and packages that load their own files at run time work exactly as installed.
The install job has 1 GB of memory, 512 MiB of temporary disk and 5 minutes. It reaches only the package registries: npm (plus nodejs.org and GitHub, where native packages download prebuilt binaries), PyPI, and the Go module proxy and checksum database (proxy.golang.org, sum.golang.org; no direct VCS fetches). package.json, requirements.txt and go.mod keep working, but inquir.yaml is the place to declare packages. A package declared in both with different versions fails the deploy instead of being merged silently; the same declaration in both is fine. A pnpm-lock.yaml or yarn.lock is not read: the platform installs with npm, and the deploy log says so.
The deploy also refuses, with a message that names the entry:
- the same package twice in the list;
- an added Node.js package that
package-lock.jsonornpm-shrinkwrap.jsondoes not pin with the same spec (update the lockfile, or use exact versions without one); - a Node.js package that
package.jsonlists underdevDependencies, because the install leaves dev dependencies out; - a
requirements.txtwith pip options (-r,-c,-e,--index-url,--hash…); ago.modthatreplaces orexcludes a listed module, or whosegoortoolchainline asks for a Go newer than the runtime's 1.22; - a source that already ships
node_modules,.python_packagesorvendor/.
Which declaration applies#
build.system and build.dependencies are looked up separately, in this order. The first declaration found wins; levels and files are never merged.
functions.<name>.buildin the function's owninquir.yamlbuildin the function's owninquir.yamlfunctions.<name>.buildin the project-rootinquir.yamlbuildin the project-rootinquir.yaml- Nothing found: the function gets no system packages and no extra language packages.
An explicit system: null, an empty block or dependencies: [] is a declaration too. It means “none” and overrides a project default.
One file, three runtimes: transcoding with ffmpeg#
The same inquir.yaml serves a Node.js, a Python and a Go function. Each handler reads FFMPEG_PATH, which the preset sets, and scales a video to 480p in a temporary directory.
# inquir.yaml (identical for Node.js, Python and Go) build: system: presets: [ffmpeg] # installs ffmpeg + ffprobe, sets FFMPEG_PATH and FFPROBE_PATH
Node.js#
const { execFile } = require('node:child_process'); const { promisify } = require('node:util'); const fs = require('node:fs/promises'); const os = require('node:os'); const path = require('node:path'); const run = promisify(execFile); exports.handler = async (event) => { const input = typeof event.body === 'string' ? JSON.parse(event.body || '{}') : event; const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'transcode-')); try { const src = path.join(dir, 'in.mp4'); const out = path.join(dir, 'out.mp4'); await fs.writeFile(src, Buffer.from(input.videoBase64, 'base64')); await run(process.env.FFMPEG_PATH || 'ffmpeg', [ '-y', '-i', src, '-vf', 'scale=-2:480', '-c:v', 'libx264', '-preset', 'veryfast', out, ]); const mp4 = await fs.readFile(out); return { statusCode: 200, body: JSON.stringify({ mp4Base64: mp4.toString('base64') }) }; } finally { await fs.rm(dir, { recursive: true, force: true }); } };
Python#
import base64 import json import os import subprocess import tempfile def handler(event, context): raw = event.get("body") payload = json.loads(raw) if isinstance(raw, str) else event with tempfile.TemporaryDirectory(prefix="transcode-") as tmp: src = os.path.join(tmp, "in.mp4") out = os.path.join(tmp, "out.mp4") with open(src, "wb") as f: f.write(base64.b64decode(payload["videoBase64"])) subprocess.run( [os.environ.get("FFMPEG_PATH", "ffmpeg"), "-y", "-i", src, "-vf", "scale=-2:480", "-c:v", "libx264", "-preset", "veryfast", out], check=True, capture_output=True, ) with open(out, "rb") as f: mp4 = base64.b64encode(f.read()).decode() return {"statusCode": 200, "body": json.dumps({"mp4Base64": mp4})}
Go#
Go system packages are coming soon; today a deploy refuses this file for a Go function. The Go runtime already runs on the same Debian bookworm base, and this handler deploys unchanged once Go support lands.
package main import ( "encoding/base64" "encoding/json" "fmt" "os" "os/exec" "path/filepath" ) func Handler(event map[string]interface{}, ctx map[string]interface{}) (interface{}, error) { payload := event if s, ok := event["body"].(string); ok && s != "" { payload = map[string]interface{}{} if err := json.Unmarshal([]byte(s), &payload); err != nil { return nil, err } } video, _ := payload["videoBase64"].(string) in, err := base64.StdEncoding.DecodeString(video) if err != nil { return nil, err } dir, err := os.MkdirTemp("", "transcode-") if err != nil { return nil, err } defer os.RemoveAll(dir) src, out := filepath.Join(dir, "in.mp4"), filepath.Join(dir, "out.mp4") if err := os.WriteFile(src, in, 0o600); err != nil { return nil, err } ffmpeg := os.Getenv("FFMPEG_PATH") if ffmpeg == "" { ffmpeg = "ffmpeg" } cmd := exec.Command(ffmpeg, "-y", "-i", src, "-vf", "scale=-2:480", "-c:v", "libx264", "-preset", "veryfast", out) if log, err := cmd.CombinedOutput(); err != nil { return nil, fmt.Errorf("ffmpeg: %v: %s", err, log) } mp4, err := os.ReadFile(out) if err != nil { return nil, err } body, _ := json.Marshal(map[string]string{"mp4Base64": base64.StdEncoding.EncodeToString(mp4)}) return map[string]interface{}{"statusCode": 200, "body": string(body)}, nil }
Build, status and deploy log#
The deploy log shows what the platform resolved, in lines that start with [system]. On a cache hit the deploy goes on at once. A new package set is built first: the deploy waits in the queue, without holding up other deploys, and resumes when the image is ready. A first build takes minutes (the ffmpeg preset builds in about 2.5 minutes); every later deploy that asks for the same set, from any function, reuses the image. If an older build of the same packages exists (after a platform base image update, for example), the deploy runs on it and the function moves to the new image when that is ready. While an image builds or after it fails, the current release keeps serving.
$ inquir deploy --asset transcode [system] presets [ffmpeg] + packages [] from inquir.yaml (nodejs22, bookworm) [system] system image: cache hit registry.inquir.org/inquir-sys/nodejs22@sha256:… +118 MB (shared) … # Declared block, image state, size, serving image and package lock $ inquir functions system transcode # The preset catalog (and the package limit for your workspace) $ inquir functions system presets
inquir functions system <function> (in the CLI release after 0.4.1) shows the declared block and the file it came from, the image state (queued, building, ready or failed), its size, which image serves the function, and the package lock: the exact version of every package the build added. The API returns the same status at GET /functions/<id>/system-image and the preset catalog at GET /functions/system-presets.
To retry a failed image, deploy again. Retries of the same package set are spaced 1, 5 and then 30 minutes apart.
Errors and fixes#
| Error | Cause | Fix |
|---|---|---|
400 invalid_inquir_yaml | The file is not valid YAML, is larger than 64 KiB, has an unknown key or a value of the wrong shape, or a build.dependencies entry is malformed or conflicts with package.json, requirements.txt or go.mod. | The message names the key or the entry. Fix it and deploy again. |
400 invalid_system_packages | An unknown preset or OCR language, an unknown key or a value of the wrong shape inside build.system, a package name that breaks the naming rule, a refused package, too many packages, or a duplicate. | Fix the name the message points to; for a misspelled preset it suggests the closest one. |
400 invalid_system_packages (Go) | build.system on a Go function: Go system packages are coming soon. | Remove the block for the Go function for now. |
400 system_packages_deploy_only | A request tried to set system packages through the function API instead of a deploy. | Declare them in inquir.yaml and deploy. |
429 system_image_quota | The workspace asked for more than 10 new package sets in 24 hours. | Reuse a package set you already built, or deploy again later. |
503 system_image_building | Code that was never deployed (a run from the editor) was invoked while its image is still building. | Retry after the Retry-After interval. |
409 system_image_failed | The same, after the image build failed. | Fix inquir.yaml and deploy again; inquir functions system <function> shows the build error. |
| Image build failed | A package does not exist in bookworm or is a virtual package, a pinned version is gone, or the added layer is larger than 1 GB. | Check the name for bookworm on packages.debian.org, update or drop the pin, or split heavy tools across functions. inquir functions system <function> shows the build error. |
| Install fails: “pkg-config not found”, “pg_config executable not found” | A native package compiles against a library whose headers are not installed. | Add the -dev package: libcairo2-dev and pkg-config for pycairo, libpq-dev for psycopg2. |
runtimeVariant: full | The Full (Chromium) image variant is retired. | Drop runtimeVariant and add packages: [chromium, fonts-liberation, fonts-noto-color-emoji] to inquir.yaml. |
Image variants and layers#
Every runtime now has one slim Debian bookworm image. The Node.js Full (Chromium) variant is retired and its selector is gone from the console. An API request that creates a function with runtimeVariant: full, or changes a function to it, is refused with 400 and a pointer to inquir.yaml; sending back the value a function already has is accepted. To get Chromium, list it as packages, as shown above.
Layers keep working for functions that use them, but they are no longer the recommended way to add dependencies: declare language packages in build.dependencies and OS packages in build.system.