Workspace artifacts
Problem
There is no API for a module to discover or resolve artifacts from its caller's workspace. Passing a Workspace object is not enough. Address lookup still uses the module's current client schema.
For example, //go:generate:container must let the Go module use a container exposed by another module in the caller's workspace. The current API cannot provide this lookup.
Discovery is also split across APIs for checks, generators, services, and shell targets. We need a common way to list artifacts and retain their addresses.
Solution
Workspace.artifacts(): enumerate and select workspace artifacts. The address model includes collection keys.Workspace.resolve(): return an Address bound to this workspace. Conversion methods, such ascontainer(), try workspace lookup before external lookup.- Breaking change: top-level
address()resolves external references only. It no longer looks up artifacts in the current client's schema.
extend type Query {
"""Resolve external references only."""
address(value: String!): Address!
}
extend type Workspace {
"""Discover workspace artifacts without evaluating their values."""
artifacts: Artifacts!
"""
Try workspace references before external resolution.
Local errors stop resolution; only absence permits fallback.
The Address retains this workspace across module calls and ID reloads.
"""
resolve(value: String!): Address!
}
"""
An immutable selection of workspace artifacts. Filters keep matches.
Listed types, collections, and keys use OR; chained filters use AND.
Empty alternatives and unknown names match nothing.
Filtering never changes addresses or collection identifiers.
Collection and key lists contain no duplicates and use a consistent order.
"""
type Artifacts {
"""Keep artifacts of any listed concrete GraphQL type."""
filterTypes(types: [String!]!): Artifacts!
"""Match one complete, ordered field sequence exactly."""
filterQuery(query: [String!]!): Artifacts!
"""Keep artifacts selected through any listed collection."""
filterCollections(collections: [String!]!): Artifacts!
"""Keep artifacts with any listed key in this collection."""
filterCollectionKeys(collection: String!, keys: [String!]!): Artifacts!
"""Collection identifiers represented in this selection."""
collections: [String!]!
"""Keys represented in this selection for the given collection."""
collectionKeys(collection: String!): [String!]!
"""Enumerate complete artifacts without evaluating their values."""
items: [Artifact!]!
"""Require exactly one match; otherwise fail."""
one: Artifact!
"""Display lines for this selection. No trailing newlines; columns may align."""
pretty: [String!]!
}
"""
One workspace value with a complete query and all required collection keys.
Distinct addresses remain distinct even when they return the same Node.
Reading metadata never evaluates the value.
"""
type Artifact {
"""
Ordered, literal fields to follow. Collection keys select items along them.
Entrypoint targets use their shorthand.
"""
query: [String!]!
"""One key per collection along the query. Unordered; empty for static artifacts."""
collectionKeys: [ArtifactCollectionKey!]!
"""Evaluate the target in the workspace that supplied this artifact."""
value: Node!
"""The full address, formatted for CLI input with consistent flag order."""
pretty: String!
}
type ArtifactCollectionKey {
"""The collection identifier, fixed across the workspace schema."""
collection: String!
"""The collection item's key."""
key: String!
}Examples
Language-neutral call sequences:
ws.artifacts().filterTypes(["Container"]).pretty()
ws.artifacts().filterQuery(["engine-dev", "container"]).one().value()
ws.resolve("engine-dev:container").container()
ws.resolve("alpine:3.22").container()
address("https://github.com/dagger/dagger#main:docs").directory()A static artifact has a query such as ["playground"] and empty collectionKeys.
With collections, the same query can reach many artifacts. Keys select one. For example, select the container for TestConnect in the Go module sdk/go:
{
"query": ["golang", "modules", "tests", "container"],
"collectionKeys": [
{"collection": "go-module", "key": "sdk/go"},
{"collection": "go-test", "key": "TestConnect"}
]
}Its pretty() output:
golang:modules:tests:container --go-module=sdk/go --go-test=TestConnectTo list test keys in one module:
ws.artifacts()
.filterCollectionKeys("go-module", ["sdk/go"])
.collectionKeys("go-test")Collection naming
Collections have keyed items. Raw lists are not collections. Use item types as collection identifiers. One primary collection keeps the short name; add context when names conflict:
| Level | Format | Example |
|---|---|---|
| 1 | <item-type> |
go-module |
| 2 | <parent-type>-<item-type> |
app-go-module |
| 3 | <parent-type>-<field>-<item-type> |
app-dependencies-go-module |
The parent type owns the collection field. Assign identifiers across the whole workspace schema, before filtering. Use actual type and field names. Do not invent role names or singularize field names.
First implementation
Start with static artifacts. Reuse the schema walker and its argument rules. Traverse module objects and stop at exposed core objects. Implement the API above with empty collectionKeys.
Add Workspace.resolve() and move module wiring to explicit workspace resolution. Preserve qualified references and entrypoint shorthands. Version-gate the change to address() and generate client bindings.
Verify listing without value evaluation, filters, unique selection, workspace settings and edits, frozen workspaces, module calls, loading by ID, and resolution errors. Existing check, generator, service, and CLI migrations can follow. Manual artifact registration is outside this design.
Remaining collection work
Before adding collections, define how to choose the primary collection, resolve remaining name conflicts, preserve identifiers across schema changes, support scalar key types, and parse collection flags.
Status: proposed design, 2026-09-16. The prototype does not yet implement this API.
Source: dagger/dagger