# Maven Builds
{{< tier level="sovereign" >}}

A Maven build can print thousands of lines. Send that to an agent or into your run history and the signal drowns. The **Maven** tool (`valdr-tools.user.maven`) from the [starter pack](../starters/) runs your build in a workflow step and returns only what matters: whether the build passed, a short summary, and the errors that caused a failure. The full log stays on the workflow host for when you need it.

Because a failed build comes back as a result, not a crash, your workflow decides what happens next. Send the errors back to the agent that wrote the code, stop for a person, or carry on. You choose the process.

It has two actions: `version` checks the Java and Maven setup, and `build` runs the goals you choose. The tool runs `./mvnw` when the working directory has one (it must be executable), otherwise `mvn` from the host's `PATH`. The workflow host needs Node, which runs the tool itself, and a JDK, plus Maven unless your project ships `mvnw`. Run `version` first to confirm the host is ready.

## Choose goals and options

The tool runs in the step's working directory, which defaults to the workflow's worktree or project repository. If your `pom.xml` lives somewhere else, set **Working directory (cwd)**. In the Builder, enter the `build` inputs in **Inputs JSON object**, for example `{"goals": ["clean", "verify"], "properties": {"skipITs": true}}`. In YAML they sit under the step's `inputs`:

```yaml
inputs:
  goals: [clean, verify]
  properties:
    skipITs: true
  profiles: [development]
  modules: [service]
  alsoMake: true
  timeoutSeconds: 240
```

- **`goals`**: any lifecycle phases or plugin goals your project defines, such as `[package]` or `[clean, verify]`. The default is `[package]`.
- **`properties`**: string, number, or boolean values, each passed as one `-Dname=value` argument, such as `test: MyTest` or `skipTests: true`.
- **`profiles`** activates Maven profiles (`-P`). **`modules`** builds only the listed modules (`-pl`); add **`alsoMake: true`** to build the modules they depend on as well (`-am`). Leave these out to build the whole project with its default profiles.
- **`javaHome`**: an optional absolute path to the JDK for this build. Otherwise Maven uses the host's Java configuration.
- **`timeoutSeconds`**: see [Set a timeout](#set-a-timeout).

Values are passed to Maven directly, never through a shell. The tool always runs Maven in batch mode without download progress or color codes, so the log stays readable.

## Set a timeout

Builds time out after 240 seconds unless you set `timeoutSeconds`, from 1 to 270. On a timeout or cancellation, Valdr attempts to stop Maven and its child processes; [detached or external work may continue](/valdr/docs/workflows/reliability-and-recovery/#cancellation). A user tool can run for at most 300 seconds, so for builds that need longer, run Maven from a [Command step](/valdr/docs/workflows/steps/command/), whose timeout you can raise, or split the build with `modules`. The first build on a fresh host also downloads dependencies, so give it extra time.

## Read the result

Every build that runs to the end returns:

| Field | What it holds |
| --- | --- |
| `result` | `passed` when Maven exits zero, `failed` when it exits nonzero |
| `summary` | One line, such as `Maven clean verify failed (exit 1).` |
| `errorExcerpt` | The first error lines Maven printed, up to ten, without its closing boilerplate. If Maven printed none, the last lines of its error output. Empty when the build passed |
| `exitCode` | Maven's exit code |
| `durationMs` | How long the build took |
| `logPath`, `logTruncated` | Where the full log is, and whether it was cut short |
| `testSummary` | The last test summary line Maven reported, when there was one |

For compile errors, the excerpt holds the file, line, and message. For test failures, it usually holds the failing test and its assertion. `testSummary` isn't a total across modules, and a passing build doesn't mean tests ran if your goals or properties skipped them.

A `failed` result covers anything that made Maven exit nonzero, including environment problems such as a misconfigured JDK or an unreachable repository. Open `logPath` when the excerpt isn't enough. The build log is an owner-only file in the host's temporary folder, capped at 2 MiB. It isn't attached to the run, and it disappears when the host clears temporary files, so copy it if you need to keep it.

The starter applies **best-effort redaction** to build output, including the excerpt and log. It may miss secrets printed by your build or plugins. You are responsible for what your build returns and logs; review and remove sensitive values before sharing output or sending it to an agent. Valdr does not apply further redaction to the tool's successful `data` result.

The step itself stops only when Maven can't run to the end: Node, Maven, or `mvnw` can't start, the build times out, or its process is killed. Fix the cause and check whether the selected goals already changed an external system before choosing **Retry step**. A retry reuses the step's inputs, so to give a build more time, raise `timeoutSeconds` and start a new run.

## Send a failed build back to the agent

Want the agent to fix its own build breaks? Add an [Outcome route](/valdr/docs/workflows/steps/condition/) after the build that loops back to the agent's session when the build fails. It's one option: the same `result` can just as easily stop for a person or let the run carry on. Choose goals and plugins that can safely run on each pass; see [retry behavior](#adapt-the-starter).

This fragment assumes an earlier session step, `launch_agent`, that started the agent in its own worktree (for example `action: launch_task` with `run: false`, so this loop sends its first turn) and maps `sessionUlid: "$.normalized.session.sessionUlid"` and `worktreePath: "$.normalized.session.worktreePath"`:

```yaml
- key: implement
  name: Implement change
  kind: session
  needs: [launch_agent]
  session:
    action: input
    sessionUlid: "${steps.launch_agent.outputs.sessionUlid}"
    prompt: |-
      Implement the change. If the last build failed, fix these errors first:
      ${runtime.loop.previous.outputs.buildErrors}
  # An input step waits for the agent's turn to finish before the build runs.

- key: build
  name: Build
  kind: tool
  needs: [implement]
  # tool: the pinned Maven build reference the Builder writes for you
  cwd: "${steps.launch_agent.outputs.worktreePath}"
  inputs:
    goals: [clean, verify]
  outputs:
    result: "$.normalized.data.result"
    errors: "$.normalized.data.errorExcerpt"

- key: route_build
  name: Route build result
  kind: condition
  needs: [build]
  checks:
    - kind: value_equals
      value: "${steps.build.outputs.result}"
      equals: passed
      domain: [passed, failed]
  outputs:
    buildErrors: "${steps.build.outputs.errors}"
  onFailure:
    action: loop_back
    to: implement
    max: 3
    exhausted: block
```

- **`implement`** sends the agent a turn in its existing session. On the first pass there's no previous build, so the error line is empty.
- **`build`** runs Maven in the agent's worktree, so it builds the agent's working copy, including changes it hasn't committed yet.
- **`route_build`** checks `result`. `domain` lists every value it can take, and on `failed`, `loop_back` returns to `implement`.
- **`max: 3`** counts every pass, including the first. If the third build still fails, `exhausted: block` stops the run for you; use `fail` to end it instead.
- **`outputs.buildErrors`** is what the next pass reads as `${runtime.loop.previous.outputs.buildErrors}`. Map `summary` or `logPath` too if you want the agent to see more.

Valdr keeps every attempt, so the run shows each implementation and build pass. When the build passes, the route completes and your workflow moves on to review, commit, or whatever comes next.

## Adapt the starter

The Maven tool is a starting point, not a policy. `goals` takes any phase or plugin goal your project defines, `install` and `deploy` included, and the tool runs what you choose.

The starter declares `build` as `retry: idempotent`, so **Retry step** is available after a tool failure such as a timeout. That declaration doesn't check what your goals or plugins do: a deployment or other external change may have completed before the failure. Review those effects before repeating the build.

In your own copy, use `retry: manual` when a tool failure after execution starts needs reconciliation. A completed build with a nonzero exit still returns `result: failed`, so your workflow decides whether to run it again. The retry setting doesn't prevent an Outcome route from looping back to the build.

Beyond the baseline environment, the tool passes only `JAVA_HOME` to Maven. `~/.m2/settings.xml` and your project's `.mvn/` configuration work as usual. If your build reads other variables, such as `MAVEN_OPTS` or repository credentials, add their names to `inheritEnv` in your copy and [supply them through `~/.valdr/environment.env` or the launch environment](../runtime/#environment-variables). Keep values out of the manifest.

When you change the tool, update its revision in both the manifest and the runner's request check, and give it your own tool ID if the copy lives in your own pack. Then [ship the new revision](/valdr/docs/workflows/user-tools/package-and-run/#ship-a-new-revision).

## Next step

[Import the starter pack](/valdr/docs/workflows/steps/user-tool/#get-the-starter-pack) and run **Maven** in a test workflow against a project you know. Using Gradle or another build tool? Copy `tools/maven` from the [starter pack source](/valdr/docs/workflows/user-tools/starters/) and [adapt it](/valdr/docs/workflows/user-tools/cli-adapters/#extend-a-starter-for-your-needs).

