skills/cache-expert/references/dagql-api-server.md
dagql is Dagger's custom GraphQL server implementation that sits on top of standard GraphQL. It enables the immutable DAG model described in the skill overview and is the main consumer of the cache layer.
Standard GraphQL only passes scalars between operations. dagql extends this by:
ID-able objects: Every installed object type gets:
id: <TypeName>ID field<TypeName>ID scalar typeload<TypeName>FromID(id: <TypeName>ID): <TypeName> field on QueryObject arguments become IDs: When a field takes an object argument, the GraphQL schema uses <TypeName>ID as the type. The ID encodes the full call chain.
Automatic caching around call identity: Results are keyed by call ID recipe digest. If a content digest exists, cache can also reuse by content match.
Server (dagql/server.go) owns installed types, schema generation, query resolution, and the session cache wrapper used during execution.
Key methods: InstallObject, InstallScalar, Resolve, Load, Select.
Typed (dagql/types.go): anything with a GraphQL type (Type() *ast.Type)ScalarType: scalar + input decoderInput: typed argument value that can encode to call literalMost of dagql's runtime value model builds on these interfaces.
Class[T] (dagql/objects.go): object type implementation and field registryField[T]: field resolver + FieldSpecFieldSpec: field metadata (Args, Type, DoNotCache, TTL, GetCacheConfig, etc.)Fields are created with Func, NodeFunc, FuncWithCacheKey, NodeFuncWithCacheKey.
ObjectType (dagql/types.go) is the runtime interface for selectable object types. Class[T] implements it.
AnyResult (dagql/types.go): interface for typed value + constructor ID + cache/lifecycle metadataAnyObjectResult (dagql/types.go): AnyResult plus selection/call methodsResult[T] (dagql/cache.go): concrete result wrapperObjectResult[T] (dagql/cache.go): selectable result wrapper with object classCurrent Result[T] model is split into:
sharedResult) reused across cache referenceshitCache, hitContentDigestCache, optional per-call ID override)That split is important for content-digest cache hits where payload is reused but caller-facing ID stays the requested recipe ID.
ID[T] (dagql/types.go) is the typed ID scalar wrapping *call.ID.
Key methods: Encode, Decode, Load, Display.
EnumValues[T] and EnumValueName for enumsInputObject[T] + InputObjectSpec for structured inputsWrapper and UnwrapAs[T] for working through wrapper layers (Result, Nullable, dynamic wrappers, etc.)Optional[I]: optional argument valuesNullable[T]: optional return valuesDynamicOptional / DynamicNullable: runtime-typed variantsThese interact heavily with selection/call normalization and result dereferencing.
Array[T], ArrayInput[I], ResultArray[T], ObjectResultArray[T]DynamicArrayOutput, DynamicArrayInputEnumerable for indexed access and nth selection behaviorFields are typically defined in schema installers under core/schema/*.
// Func: receives unwrapped self value
dagql.Func("fieldName", func(ctx context.Context, self *core.Container, args MyArgs) (*core.Container, error) {
return self, nil
})
// NodeFunc: receives ObjectResult[T], so you can inspect ID and metadata
dagql.NodeFunc("fieldName", func(ctx context.Context, self dagql.ObjectResult[*core.Container], args MyArgs) (*core.Container, error) {
_ = self.ID()
return self.Self(), nil
})
Use NodeFunc when you need ID-aware behavior.
By default, returned values get the field call ID (receiver + field + args).
When needed, you can return a result with different identity:
ObjectResult[T]/Result[T] built from a different ID or digest.WithContentDigest) for content-based cache reuse.This is the core tool for sharing work across different query shapes that produce equivalent results.
GetCacheConfig)FuncWithCacheKey / NodeFuncWithCacheKey install a GetCacheConfig callback:
func(ctx context.Context, self ObjectResult[T], args A, req GetCacheConfigRequest) (*GetCacheConfigResponse, error)
Prebuilt helpers (dagql/cachekey.go):
| Function | Behavior |
|---|---|
CachePerClient | Mixes client ID into cache key digest |
CachePerSession | Mixes session ID into cache key digest |
CachePerCall | Uses random digest (effectively no reuse for that call identity) |
CachePerSchema | Mixes schema digest |
CachePerClientSchema | Mixes client + schema digests |
Important behavior in preselect:
CacheKey.CacheKey.ID nil, dagql uses the original computed ID.This keeps execution args, telemetry, and cache identity aligned.
When a query executes:
Server.Resolve
-> ObjectResult.Select
-> preselect (build ID + cache key)
-> call (SessionCache.GetOrInitCall)
preselectObjectResult.preselect (dagql/objects.go):
receiver.Append(...))CacheKey from ID + field spec (TTL, DoNotCache, ConcurrencyKey)GetCacheConfig if configuredcallObjectResult.call (dagql/objects.go):
SessionCache.GetOrInitCallPostCall before returningSessionCache (dagql/session_cache.go) wraps base cache (dagql/cache.go).
Base cache key input is:
type CacheKey struct {
ID *call.ID
ConcurrencyKey string
TTL int64
DoNotCache bool
}
Derived behavior:
ID.Digest()ID.ContentDigest()(callKey, ConcurrencyKey)Session wrapper responsibilities:
DoNotCache (noCacheNext)For content-digest hits, cache reuses payload but preserves the caller's requested recipe ID in the returned result metadata.
Typical pattern:
func (s *containerSchema) Install(srv *dagql.Server) {
dagql.Fields[*core.Query]{
dagql.Func("container", s.container),
}.Install(srv)
dagql.Fields[*core.Container]{
dagql.Func("withEnvVariable", s.withEnvVariable).Args(...),
dagql.NodeFunc("from", s.from),
}.Install(srv)
}
Useful helpers while debugging/exploring execution:
| Function | Purpose |
|---|---|
CurrentID(ctx) | ID currently being evaluated |
CurrentDagqlServer(ctx) | Current server |
NewResultForCurrentID(ctx, val) | Wrap value in Result using current ID |
NewObjectResultForCurrentID(ctx, srv, val) | Wrap value in ObjectResult using current ID |
| File | Contents |
|---|---|
dagql/server.go | Server, Resolve, Load, Select, InputObject |
dagql/objects.go | Class, Field, FieldSpec, preselect, field call wiring |
dagql/cache.go | base cache, CacheKey, Result, ObjectResult |
dagql/types.go | core interfaces/types (Typed, Input, AnyResult, ID[T], arrays, enums) |
dagql/nullables.go | optional/nullable wrappers |
dagql/builtins.go | builtin conversions, dynamic wrappers |
dagql/cachekey.go | cache key rewrite helpers |
dagql/session_cache.go | session cache wrapper |
core/schema/*.go | concrete API implementations |
Func vs NodeFunc: default to Func; use NodeFunc when ID-aware logic is needed.GetCacheConfig ID rewrites are authoritative: returned ID controls cache identity and argument decode.DoNotCache skips reuse for that call path but still returns a normal result value.ConcurrencyKey, not global across clients.