Dagger for PureScript #
Write Dagger pipelines and Dagger modules in PureScript.
- Pure, immutable builders composed with
#. Affat execution boundaries, with cancellation and timeouts.- Distinct object types, custom scalar newtypes, enums, and
Mayberesults. - Typed option records with defaults; object arguments resolve their IDs lazily.
- 924 generated API fields across 97 modules.
- Module registration and invocation, typed arguments, constructors, and stateful methods.
Experimental. Targets Dagger v1.0.0-beta.21. Client and module protocol unit tests pass, and live client container/filesystem operations have been verified against the beta engine. Workspace scaffolding and module greeting/container functions also pass, including a PureScript handler calling a Gleam dependency through generated bindings. See status and limits.
Get started #
You need Node.js 22+, PureScript 0.15.16, and Spago 1.0.5. Dagger and a working container engine are needed for real pipelines and module integration tests.
From this directory:
npm install
npm run build
npm test
The repository pins the PureScript package set in spago.yaml and dependency
resolution in spago.lock. The npm development tools are pinned in package.json.
Run the engine smoke test:
dagger api with-session -- npm run test:integration
This runs a container, creates and reads a file, then mounts a directory into a
container. It is a small working pipeline you can copy from
test/Test/Integration.purs.
Use the client in your project #
The SDK is not published to the PureScript registry yet. Add an external checkout
as a path dependency in your project's spago.yaml:
package:
name: my-pipeline
dependencies:
- aff
- console
- dagger-sdk
- effect
- prelude
workspace:
packageSet:
registry: 77.8.0
extraPackages:
dagger-sdk:
path: ../dagger-sdk/purescript
The example assumes the SDK checkout is outside your project's workspace. When
vendoring a package inside a Spago workspace, use a package-only manifest for
that package and let Spago discover it; do not also list it under extraPackages.
The module bootstrap handles this for you.
Call an installed module from PureScript #
The examples use the hello-gleam module generated by this repository's
Gleam SDK. It exposes hello(name: String): String and
containerEcho(message: String): Container. Its implementation language does not
restrict callers: both SDKs can call the same module.
Create the dependency inside this checkout, whose dagger.toml registers both SDKs:
export DAGGER_SDK_DIR=/absolute/path/to/dagger-sdk
cd "$DAGGER_SDK_DIR"
dagger module init gleam --name hello-gleam --path ./hello-gleam -y
Each caller below installs it as a dependency. dagger generate -y generates
dagger/hello_gleam or Dagger.HelloGleam and the matching root client accessor.
These bindings are specific to the installed dependency; they are not part of
the SDK's bundled core schema. Do not substitute a same-named remote module
without checking its API.
From the same directory, create a PureScript caller and install the dependency:
dagger module init purescript --name caller-purescript --path ./caller-purescript -y
cd caller-purescript
dagger module client add ../hello-gleam --sdk=purescript -y
dagger generate -y
Replace src/Main.purs with:
module Main where
import Prelude
import Dagger.Container as Container
import Dagger.HelloGleam as HelloGleam
import Dagger.Modules as Module
import Dagger.Query as Query
import Effect (Effect)
main :: Effect Unit
main = Module.runModule
[ Module.object_ "CallerPurescript" {}
[ Module.function "hello" "Call the dependency's greeting function."
(Module.argument "name")
(\client name -> Query.helloGleam client # HelloGleam.hello name)
, Module.function "echoMessage" "Read output from a dependency's container."
(Module.argument "message")
(\client message -> Query.helloGleam client
# HelloGleam.containerEcho message
# Container.stdout)
]
]
dagger api call -m . hello --name developer
dagger api call -m . echo-message --message "Called from PureScript"
HelloGleam.hello calls the installed module and returns Aff String.
HelloGleam.containerEcho calls its container-returning function; Container.stdout
then executes the selection and returns the output. Module.runModule supplies
the connected client to each handler, so no nested CLI process is needed.
The caller scaffold configures its generated SDK package automatically. The
local-path manifest above is for standalone applications using the bundled core
bindings. Standalone module callers must explicitly load/serve the module and
generate bindings for its schema. dagger api with-session only provides a session; it does
not install modules or generate Dagger.HelloGleam.
For standalone pipelines, use Dagger.withConnection under dagger api with-session to read
DAGGER_SESSION_PORT and DAGGER_SESSION_TOKEN. The CLI owns the session; the SDK
attaches to it. For an existing session you can also use
Dagger.connectWith { port, token, timeoutMs }.
The client API #
Builders are pure; leaves execute #
Query.helloGleam, HelloGleam.containerEcho, and Container.withExec construct
immutable query paths. HelloGleam.hello is a scalar module function and returns
Aff String. Container.stdout, File.contents, and other scalar leaves return
Aff. Reusing a builder does not mutate it or cache its result.
Use qualified imports to keep names such as id, file, and directory clear.
Types and enum constructors live in Dagger.Types. Reserved PureScript words
receive a trailing underscore, for example Query.module_ and Container.import_.
Options use record updates #
import Data.Maybe (Maybe(..))
import Dagger.Types (ReturnType(..))
-- Inside a handler in the caller above:
let opts = Container.defaultWithExecOpts
{ stdin = Just "hello\n"
, expect = Just ReturnTypeAny
, useEntrypoint = Just false
}
output <- Query.helloGleam client
# HelloGleam.containerEcho "from the dependency"
# Container.withExecWith [ "cat" ] opts
# Container.stdout
This calls the dependency's container-returning function before adding an exec
with options. The short Container.withExec function uses engine defaults. The With variant accepts an options
record. Nothing omits an argument; Just false, Just 0, and empty arrays are
preserved. Nullable response values use Maybe.
Pass Dagger objects directly #
import Dagger.Directory as Directory
let source = Query.directory client
# Directory.withNewFile "hello.txt" "hello"
output <- Query.helloGleam client
# HelloGleam.containerEcho "from the dependency"
# Container.withMountedDirectory "/src" source
# Container.withExec [ "cat", "/src/hello.txt" ]
# Container.stdout
The container comes from a module function; the directory is a core Dagger
object. Both compose in the same typed pipeline. Object IDs are resolved when
the request executes. Object-list results return
Aff (Array ObjectType); each object can be used in subsequent typed selections.
Errors #
Use ordinary Aff error handling, or Dagger.Errors.tryDagger to obtain an
Either DaggerError a. Its constructors are:
ExecFailure: command, exit code, stdout, stderr, and message.GraphQLFailure: the full list of GraphQL errors, including paths and extensions.RequestFailure: transport, timeout, configuration, or decoding failure messages.
Cancelling an Aff request aborts its HTTP fetch. Requests time out after five
minutes by default. Arguments are sent as GraphQL variables, never interpolated
into the query text. Authentication is sent only to the loopback session endpoint;
redirects are rejected.
For an API outside your generated bindings, Dagger.Connection exposes sessions
and rawQuery, which accepts a query string and Argonaut JSON variables.
Write a Dagger module #
The custom SDK is described by dagger-module.toml. Its small Go adapter
builds a Node.js/PureScript runtime, generates bindings from Dagger's supplied
schema, scaffolds a Spago package, and compiles your module. Your module and its
registration/invocation runtime are PureScript.
Use the registered SDK from this checkout:
cd /absolute/path/to/dagger-sdk
dagger module init purescript --name hello-purescript --path ./hello-purescript -y
cd hello-purescript
dagger api call -m . hello --name developer
dagger api call -m . container --message "Hello from a module!" stdout
The scaffold creates src/Main.purs with a greeting and a function returning a
Dagger container. You can inspect the exact template
here. Main.main calls Module.runModule.
Infer argument and return types from handlers #
module Main where
import Prelude
import Dagger (Client)
import Dagger.Modules as Module
import Effect (Effect)
import Effect.Aff (Aff)
greet :: Client -> { name :: String, greeting :: String } -> Aff String
greet _ { name, greeting } = pure (greeting <> ", " <> name <> "!")
main :: Effect Unit
main = Module.runModule
[ Module.object_ "HelloPurescript" {}
[ Module.function "greet" "Greet someone by name."
(ado
name <- Module.argument "name"
greeting <- Module.defaultArgument "greeting" "Hello"
in { name, greeting })
greet
]
]
To call this greet function from another PureScript module, install the
hello-purescript module there and run dagger generate -y. The consumer imports
Dagger.HelloPurescript as HelloPurescript and invokes it in a handler:
-- Inside a consumer handler, after installing hello-purescript:
-- import Data.Maybe (Maybe(..))
-- import Dagger.HelloPurescript as HelloPurescript
Query.helloPurescript client
# HelloPurescript.greetWith "developer"
(HelloPurescript.defaultGreetOpts { greeting = Just "Hi" })
-- Aff String
Arguments is an Applicative, so ado builds a typed record without stringly
typed argument lookups in your handler. The handler receives a connected client
and the decoded argument value. Its return type determines the registered Dagger
return type. Use pure unit for a function with no arguments.
Supported values include String, Int, Number, Boolean, Unit, arrays,
Maybe, generated enums/scalars, and core Dagger objects such as Container,
Directory, and File. Maybe arguments may be omitted. A defaultArgument
registers the default with Dagger and uses it during dispatch when absent.
Object inputs are reconstructed from IDs; object results are returned as IDs.
The main object's name must match the PascalCase module name, such as
hello-purescript → HelloPurescript. Registration is explicit and checked by the
PureScript compiler: no decorators, source-text scanning, or unsafe casts.
Constructors and stateful methods #
Use a JSON-encodable PureScript record for object state:
Module.object_ "Counter" { count: 0 }
[ Module.constructor (Module.defaultArgument "start" 0)
(\_ start -> pure { count: start })
, Module.method "value" "Read the counter." (pure unit)
(\_ state _ -> pure state.count)
, Module.stateful "increment" "Return the incremented counter." (pure unit)
(\_ state _ -> pure (state { count = state.count + 1 }))
]
If this is the root object of a module named counter, install that module in
your consumer and regenerate bindings. A handler can then call it from code:
-- import Data.Maybe (Maybe(..))
-- import Dagger.Counter as Counter
Query.counterWith (Query.defaultCounterOpts { start = Just 10 }) client
# Counter.increment
# Counter.value
-- Aff Int: 11
State is decoded from the parent object supplied by Dagger on each invocation.
stateful returns new state as the same Dagger object type, allowing CLI chains
such as dagger api call -m . increment value. If you omit a constructor, the initial
state passed to object_ becomes a no-argument constructor.
Register multiple object definitions in the array passed to runModule when
needed. Advanced custom values can implement DaggerValue; their named object
definitions must also be registered. Core enum references are supported; declaring
new module enums and interfaces is not yet provided by the authoring API.
Module development #
Run dagger generate -y after changing dependencies or API definitions. Generated
bindings must be committed under dagger-sdk/, and Spago discovers this package as part
of the workspace. The bootstrap preserves an existing spago.yaml and
src/Main.purs; for an existing project, add dagger-sdk to your package's
dependencies yourself.
The supplied engine schema includes installed module dependencies, so codegen can
produce their client bindings as well. Avoid naming dependency types after
handwritten SDK modules such as Modules, Connection, or Errors; codegen
reports a collision rather than overwriting those modules.
To run the actual module end-to-end test:
npm run test:modules
It creates a temporary module using this custom SDK, invokes its greeting, then
calls through a returned container to stdout. It cleans up its temporary source
directory afterward. This test needs Docker and network access to install the
compiler and package dependencies in the runtime image.
Work on the SDK #
| Command | What it does |
|---|---|
npm run build |
Compile all SDK modules and examples in the test tree |
npm test |
Run transport/scaffold tests, PureScript tests, and generated-file checks |
npm run generate |
Regenerate from the checked-in introspection snapshot |
npm run check-generated |
Check generated files without modifying them |
dagger api with-session -- npm run test:integration |
Exercise client operations against an engine |
npm run test:modules |
Build and call a PureScript module through Dagger |
To generate against another schema:
node scripts/generate.mjs /path/to/introspection.json
The generator accepts a schema object, { "__schema": ... }, or a standard
{ "data": { "__schema": ... } } response. Custom input replaces the generated
bindings in this checkout; run npm run generate to restore the bundled snapshot.
Do not edit generated modules directly. Full schema provenance and licensing are
in schema/README.md.
Project map #
src/Dagger.purs Client entry point
src/Dagger/*.purs Generated API modules, types, and public helpers
src/Dagger/Internal/ Lazy selections and Node HTTP transport
src/Dagger/Module/ Typed definitions, value codecs, invocation protocol
scripts/ Codegen, scaffolding, and live module smoke test
runtime/ Custom SDK adapter and module template
schema/ Upstream introspection snapshot and provenance
test/ Transport, client, module, and integration tests
Status and limits #
This is an initial implementation, not an official Dagger SDK release.
- Client and module protocol tests pass with mocked HTTP, and a freshly scaffolded module compiles with zero warnings. Live client operations, workspace scaffolding, and module greeting/container functions pass against beta.21.
- The custom SDK manifest requests engine version
v1.0.0-beta.21. The bundled client bindings come from that released engine. Rundagger generate -yafter changing dependencies and commit generated bindings; execution uses those files. - Workspace-managed standalone client scopes are not yet supported; standalone programs can use the core SDK and explicit schema generation.
- Sessions must already exist (
dagger api with-session, module invocation, or explicit credentials). The SDK does not download a CLI or provision an engine. - Module state is available to methods; automatic exposed object fields, custom enum/interface declarations, source maps, decorators, and telemetry are not yet implemented.
- The generator currently requires object lists to be non-null lists with non-null elements, matching the bundled schema. Unsupported object-list shapes fail generation explicitly.
- Query execution is not memoized. Resolving object arguments or materializing object lists may require additional requests.
Examples and continuous integration #
Explore the runnable examples for a container pipeline and a Dagger module with named, typed handlers. The examples target the root mise pin, Dagger 1.0.0-beta.21.
GitHub Actions runs this SDK’s tests, checks generated bindings, and exercises the pipeline against a live engine. From the repository root, you can also run the module smoke test:
mise exec -- python3 scripts/test-module.py purescript
License #
Original SDK code is MIT licensed. The upstream Dagger schema and API descriptions retain their Apache-2.0 license; see NOTICE, schema/LICENSE, and schema/NOTICE.