Skip to content
Docs

inquir.yaml: build configuration for functions

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.yamlbash
# 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.

KeyTypeWhat it does
buildmapBuild settings for every function deployed from this directory or project.
build.systemmap or nullOS packages installed into the function image (Debian bookworm). null or an empty block declares “no packages”.
build.system.presetslist, up to 8Platform presets by name. Only tesseract takes options: { name: tesseract, langs: [deu, rus] }.
build.system.packageslist, up to 10Extra Debian packages by apt name, optionally pinned as name=version.
build.dependencieslist, up to 200Language 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>.buildmapThe 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.

PresetInstallsEnvironment
ffmpegffmpeg: ffmpeg and ffprobe (FFmpeg 5.1)FFMPEG_PATH=/usr/bin/ffmpeg
FFPROBE_PATH=/usr/bin/ffprobe
imagemagickimagemagick (ImageMagick 6: convert, identify), ghostscript, fonts-dejavu-core. Debian's ImageMagick policy keeps PDF and PostScript disabled.—
libvipslibvips-tools, libvips42: the vips command and the library for pyvips (Node's sharp ships its own libvips)—
popplerpoppler-utils: pdftotext, pdftoppm, pdfinfo—
tesseracttesseract-ocr 5 and tesseract-ocr-eng, plus tesseract-ocr-<lang> for each entry in langsTESSDATA_PREFIX=/usr/share/tesseract-ocr/5/tessdata
ghostscriptghostscript (gs), the PostScript and PDF interpreter—
fontsfonts-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 apt knows 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 list build-essential, not g++. Each name must be a real package: the build checks every name with apt-cache before 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 example tesseract-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.yamlbash
# 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
index.jsjs
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 in package.json.
  • Python: a pip requirement with a version specifier (requests==2.32.3, uvicorn[standard]>=0.30). Environment markers and direct URL references belong in requirements.txt.
  • Go: module@version with an exact version or pseudo-version (github.com/google/uuid@v1.6.0). Modules at v2 and later need the /vN suffix 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.json or npm-shrinkwrap.json does not pin with the same spec (update the lockfile, or use exact versions without one);
  • a Node.js package that package.json lists under devDependencies, because the install leaves dev dependencies out;
  • a requirements.txt with pip options (-r, -c, -e, --index-url, --hash…); a go.mod that replaces or excludes a listed module, or whose go or toolchain line asks for a Go newer than the runtime's 1.22;
  • a source that already ships node_modules, .python_packages or vendor/.

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.

  1. functions.<name>.build in the function's own inquir.yaml
  2. build in the function's own inquir.yaml
  3. functions.<name>.build in the project-root inquir.yaml
  4. build in the project-root inquir.yaml
  5. 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.yamlbash
# inquir.yaml (identical for Node.js, Python and Go)
build:
  system:
    presets: [ffmpeg]   # installs ffmpeg + ffprobe, sets FFMPEG_PATH and FFPROBE_PATH

Node.js#

index.jsjs
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#

handler.pypython
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.

main.gogo
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.

terminalbash
$ 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#

ErrorCauseFix
400 invalid_inquir_yamlThe 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_packagesAn 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_onlyA request tried to set system packages through the function API instead of a deploy.Declare them in inquir.yaml and deploy.
429 system_image_quotaThe 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_buildingCode 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_failedThe same, after the image build failed.Fix inquir.yaml and deploy again; inquir functions system <function> shows the build error.
Image build failedA 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: fullThe 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.