View all results

Language Overview

FQL is a small, expression-oriented language for extracting, transforming, and returning structured data. It’s designed to be embedded in tools and runtimes that determine what a script can access.

Scripts produce values

Every useful FQL script ends by producing a result. The most direct way is with RETURN:

example.fql Ferret v2
query.fql
FQL
LET price = 19.99 LET quantity = 3 RETURN { price: price, quantity: quantity, total: price * quantity }

A script can also end with a top-level FOR expression, in which case the result is the array that FOR produces. Either way, FQL scripts communicate through returned values rather than printed output. That makes the same script usable across different surfaces: a CLI can serialize the result as JSON, an embedded application can receive it as a Go value, and a test runner can assert against it directly. The script does not need to know who is consuming the result.

Expressions are the main building block

FQL is expression-oriented: most pieces of logic produce a value. Arithmetic, object, and array literals, conditionals, function calls, queries, and waiting constructs all evaluate to values.

That means expressions can usually be assigned to variables, returned from scripts, passed to functions, or nested inside larger expressions.

example.fql Ferret v2
query.fql
FQL
LET score = 87 RETURN score >= 80 ? "passed" : "failed"

LET is how you name an intermediate value. A typical script uses LET to break logic into readable steps - get something, transform it, return the result - rather than nesting everything into one large expression.

example.fql Ferret v2
query.fql
FQL
LET user = { name: "Ada", roles: ["admin", "editor"] } LET isAdmin = CONTAINS(user.roles, "admin") RETURN { name: user.name, isAdmin: isAdmin }

Collections are transformed with FOR

FOR is not a general-purpose loop - it is a data-shaping construct. It iterates over a collection and returns a new collection, optionally filtering and transforming along the way.

example.fql Ferret v2
query.fql
FQL
LET users = [ { name: "Ada", active: true }, { name: "Grace", active: false }, { name: "Linus", active: true } ] FOR user IN users FILTER user.active RETURN user.name

This script starts with an array of users and returns only the names of active ones. The shape of the data changes; the logic remains declarative. FOR expressions can appear inline too, assigned via LET and composed with the rest of the script:

example.fql Ferret v2
query.fql
FQL
LET page = WEB::HTML::OPEN("https://mockery.ferretlang.org/scenarios/ecommerce/products/") FOR item IN page[~ css`.product-card`] FILTER item.attributes["data-in-stock"] == "true" RETURN { title: item[~? css`.product-title`].textContent, url: item[~? css`a`].attributes.href }

Even when the source is messy, the final result can be structured and clean.

Queries operate on capable values

FQL’s query syntax is not tied to one data type or one library. Instead, querying is capability-based: a value can support one or more query dialects, and FQL can query that value using whichever dialect is appropriate.

An HTML object might support both CSS and XPath:

example.fql Ferret v2
query.fql
FQL
LET page = WEB::HTML::OPEN("https://mockery.ferretlang.org/scenarios/ecommerce/products/") LET links = page[~ css`a[href]`] RETURN links

The long form of a query expression makes the structure explicit, and supports passing query-specific data or options:

example.fql Ferret v2
query.fql
FQL
LET db = DB::SQLITE::OPEN({ memory: true }) LET create = QUERY ONE ` CREATE TABLE users ( id INTEGER PRIMARY KEY, name TEXT NOT NULL ) ` IN db USING sql_exec LET insert = QUERY ONE ` INSERT INTO users(name) VALUES (?) ` IN db USING sql_exec WITH { params: ["Ada"] } OPTIONS { transactionLevel: "read-uncommitted" } RETURN db[~ sql`SELECT id, name FROM users`]

The meaning of WITH and OPTIONS is defined by the value being queried and the selected dialect. The language provides a uniform syntax for query expressions; the runtime value provides the actual query behavior. This means FQL does not need built-in syntax for every possible source - HTML documents, databases, browser elements, and custom objects can all expose query capabilities while reusing the same FQL syntax.

Waiting is explicit

Dynamic workflows often involve timing: a page may not have finished loading, an element may appear only after JavaScript runs, or a value may change in response to an event. FQL includes waiting constructs for these cases, and they are first-class expressions rather than library utilities.

example.fql Ferret v2
query.fql
FQL
LET page = WEB::HTML::OPEN("https://mockery.ferretlang.org/scenarios/network/delayed-requests/", { driver: "cdp" }) RETURN WAITFOR VALUE page[~ css`.network-result-card p`] TIMEOUT 5s ON TIMEOUT RETURN false

This expression polls every 250ms, gives up after 5s, and returns false if the timeout is reached. A script that waits forever is very different from a script that waits five seconds and returns a fallback - FQL makes that difference visible in the source rather than hiding it in configuration or library code.

Modules provide runtime behavior

FQL is intentionally small. The core language defines the syntax and execution model; modules and the host runtime supply the capabilities.

A module can provide namespaced functions:

example.fql Ferret v2
query.fql
FQL
RETURN YAML::DECODE(` name: Ada roles: - admin - editor `)

A module can also provide value types with query support - the HTML module, for example, exposes DOCUMENT, PARSE functions, and the css, xpath query dialects. Other modules can provide integrations with file formats, external APIs, browser runtimes, databases, and custom application objects. The language stays consistent regardless of which modules are present; the host environment determines what is available.

This means the same script might behave differently in a CLI context than in a browser automation runtime, not because the language changes, but because the set of registered modules and host capabilities differs. This is similar to how a SQL query depends on the database engine it runs against, or how a JavaScript file behaves differently in a browser versus Node.js.

Where to go next