Host Values
Host values let an embedding application expose Go-managed resources and objects to FQL scripts. This page covers how to implement them in Go. For the FQL perspective — what host values look like inside a script and which operations they support — see Host Values and Capability Types.
When to implement a host value
- Resource handle — the Go object owns an external resource (database connection, file handle, HTTP client) whose lifecycle outlives a single function call.
- Lazy value — the value materializes data only when accessed, avoiding upfront cost (cursor, paginated result set).
- Domain object — the value has rich behavior (property access, iteration, dispatch) that cannot be expressed as a plain array or object.
- External client — the value wraps a Go client library and exposes its operations through FQL capabilities (query, dispatch, subscribe).
- Controlled capabilities — you want to expose only certain operations (read-only properties, no iteration) rather than giving scripts full access to the underlying data.
Choosing the right mechanism
| Mechanism | Best for | Script sees | Lifecycle |
|---|---|---|---|
| Parameter | Static data: strings, numbers, config, JSON-like structures | @name — a plain value |
Host sets before execution |
| Function | Stateless computation: hash, format, fetch | FN(args) → result |
No state between calls |
| Host value | Stateful resource or behavioral object | Variable with capabilities: val.prop, FOR x IN val, QUERY ... IN val |
Host creates; runtime tracks io.Closer |
| Module | Self-contained extension bundling functions + hooks | Namespaced functions and values | Module Register + engine lifecycle hooks |
When in doubt, walk through these questions:
| Question | If yes | If no |
|---|---|---|
| Does the Go object own a resource that must be closed? | Host value (implement io.Closer) |
↓ |
| Does the script need to access properties, iterate, query, or dispatch? | Host value (implement capability interfaces) | ↓ |
| Is the data static and known before execution? | Parameter (WithRuntimeParam) |
↓ |
| Is it a one-shot computation with no state? | Function | ↓ |
| Does it bundle multiple functions + hooks? | Module | Function or host value |
The Value interface
Every host value must satisfy runtime.Value:
String()— text representation used byTO_STRING()and encoding fallbacks.Hash()— identity hash used by the VM for deduplication and map keys. Usefnv.New64afrom the standard library.Copy()— shallow copy. For host values that wrap a pointer, returning the same pointer is usually correct.
Optionally implement Typed so the runtime can report your type in error messages and enable generic argument casting with CastArg:
Create a type with runtime.NewTypeFor:
Minimal implementation
The smallest possible host value — a struct that wraps a string:
Register a function that returns it:
Adding capabilities
A host value without capability interfaces is opaque — scripts can hold it and pass it around, but cannot access properties, iterate over it, or query it. Implement capability interfaces to enable FQL operations on the value.
Property and index access
| Interface | Method | FQL syntax |
|---|---|---|
KeyReadable |
Get(ctx, key Value) (Value, error) |
val.prop, val["key"] |
KeyLookup |
Lookup(ctx, key Value) (Value, bool, error) |
val?.prop |
KeyWritable |
Set(ctx, key, value Value) error |
val.prop = x |
KeyRemovable |
RemoveKey(ctx, key Value) error |
DELETE val.prop |
IndexReadable |
At(ctx, idx Int) (Value, error) |
val[0] |
IndexLookup |
LookupAt(ctx, index Int) (Value, bool, error) |
val?[0] |
IndexWritable |
SetAt(ctx, idx Int, value Value) error |
val[0] = x |
IndexRemovable |
RemoveAt(ctx, idx Int) (Value, error) |
DELETE val[0] |
Adding property access to the Label type:
Iteration and measurement
| Interface | Method | FQL syntax |
|---|---|---|
Iterable |
Iterate(ctx) (Iterator, error) |
FOR x IN val |
Iterator |
Next(ctx) (value, key Value, err error) |
(returned by Iterate) |
Measurable |
Length(ctx) (Int, error) |
LENGTH(val) |
Containable |
Contains(ctx, value Value) (Boolean, error) |
x IN val |
Iterator.Next must return io.EOF when the sequence is exhausted. If the iterator holds resources (an open cursor, for example), implement io.Closer on it as well.
Query, dispatch, and observe
| Interface | Key method | FQL syntax |
|---|---|---|
Queryable |
Query(ctx, Query) (List, error) + 3 modifiers |
QUERY ... IN val USING ... |
Dispatchable |
Dispatch(ctx, DispatchEvent) error |
DISPATCH "event" TO val / val <- event |
Observable |
Subscribe(ctx, Subscription) (Stream, error) |
WAITFOR EVENT "name" IN val |
The Queryable interface has four methods: Query, QueryOne, QueryCount, and QueryExists. If your implementation only needs the list-returning Query, delegate the other three to the built-in helpers:
The Query struct carries the expression, dialect, parameters, and options from the FQL statement:
Comparison and other capabilities
| Interface | Method | Purpose |
|---|---|---|
Comparable |
Compare(other Value) int |
Enables ==, <, >, and SORT |
Sortable |
SortAsc(ctx) error, SortDesc(ctx) error |
In-place sort |
Cloneable |
Clone(ctx) (Cloneable, error) |
Deep copy |
Unwrappable |
Unwrap() any |
Extract the inner Go value |
DebugInspectable |
DebugInfo() DebugInfo |
Presentation hints for the debugger |
Unwrappable is useful when other Go code (custom functions, modules) needs to access the underlying Go object. The runtime.UnwrapAs generic helper simplifies extraction:
Returning host values from functions
A common pattern is a namespaced function that opens a resource and returns it as a host value. The following example implements a minimal in-memory store that supports QUERY ... IN:
Passing host values as parameters
Instead of creating a host value inside a function, you can pass one directly as a parameter with WithRuntimeParam or WithSessionRuntimeParam:
This is useful when the host application controls the resource lifecycle and wants to share a single instance across multiple query executions. The engine-level parameter is available to every session without re-creation.
For per-session values, use WithSessionRuntimeParam:
Cleanup and finalization
The runtime automatically tracks values that implement io.Closer:
- During execution, any
io.Closervalue encountered in the result is added to a tracked closer set. - After encoding the output, the runtime calls
Close()on every tracked closer. - Errors from
Close()are propagated to the caller ofSession.Run.
This means a host value returned by a function inside a script is encoded first, then closed. The host receives the encoded output; the value is already closed by the time Run returns.
For values referenced multiple times, implement the Resource interface to prevent double-closing:
ResourceID must return a stable, unique identifier for the resource. The runtime uses it to deduplicate closers — two references to the same resource result in a single Close call.
Values stored in engine-level parameters are not tracked by the result’s closer set. The host owns their lifecycle and must close them explicitly when the engine shuts down.
Error behavior
When a capability method returns a non-nil error, the VM raises an FQL runtime error at the point of the operation. The error message includes the value’s type and the Go error text. FQL scripts can recover from these errors with ON ERROR .... See Error Handling.
When the script attempts an operation that requires a capability the value does not implement (for example, FOR x IN val on a non-Iterable value), the VM raises a type error listing the expected capability.
Use the runtime error helpers to return well-formatted errors from your capability methods:
The Proxy shortcut
For cases where a Go type already implements some capability interfaces, sdk.Proxy[T] avoids the boilerplate of implementing Value manually. The proxy wraps any Go value and delegates capability calls via type assertion:
If *MyDB implements Queryable, calls to proxy.Query delegate to myDB.Query. If it does not implement Iterable, proxy.Iterate returns a typed error. The proxy also handles Value methods (String, Hash, Copy), Typed, Unwrappable, json.Marshaler, and Comparable delegation automatically.
For Go maps and slices, use the specialized variants:
ProxyMap automatically implements KeyReadable, KeyWritable, and Iterable for map[string]V types. ProxySlice implements IndexReadable, IndexWritable, and Sortable.