Requests and Execution
The tool protocol is deliberately small: one process, one JSON request in, one JSON result out. There’s no long-lived server to keep alive and no SDK to track, so any script you can test from a terminal can become a workflow step.
One request in, one result out
Each time a step runs, Valdr starts your program and writes one UTF-8 JSON request to stdin, then closes it:
{
"protocolVersion": 1,
"toolId": "my-tools.user.text",
"toolRevision": "1.0.0",
"action": "summarize",
"input": {"text": "Hello Valdr\nTools are ready."},
"operationId": "example-operation",
"attemptId": "example-attempt"
}Your program reads to the end of stdin, checks the request, runs the chosen action, and writes one JSON object to stdout.
A success:
{"ok":true,"data":{"characters":28,"words":5,"lines":2}}A handled failure:
{"ok":false,"error":{"code":"unsupported_action","message":"Unknown action."}}Valdr places successful data under $.normalized.data, which is what workflow output mappings read.
Rules for a clean result
- Exactly one JSON object on stdout. No banners, progress lines, JSON Lines, or streaming chunks. Put logs on stderr.
- Exit zero, even for handled failures. A nonzero exit is treated as a crashed process, and Valdr won’t read your error envelope.
- Match the envelope exactly. Success has only
okanddata. Failure has onlyokanderror, whereerrorneeds a non-emptycodeandmessage. - Report child CLI failures. When a wrapped CLI fails, add its
exitCode(a whole number ornull) andsignal(such asSIGTERM, ornull) toerror. Leave them out for failures that didn’t involve a child process. datamust match the action’soutputSchema.data: nullis fine if the schema allows it. Encode binary data in JSON, or write it to a file and return its path.
Invalid UTF-8, extra output, or data that fails the schema marks the attempt as failed.
What’s in the request
input is exactly what the workflow author supplied, kept separate from Valdr’s own fields. Valdr adds no project key, path, or workflow context to the request. If an action needs a repository name, a cloud project, or a Jira issue, declare it as an ordinary input.
operationId stays the same when a step is retried, so you can use it to recognize a repeat. attemptId changes on every attempt. A new run or loop iteration gets a new operationId.
Where your program runs
Your program runs on the workflow host as your user. If the run was started from an agent session, the working directory is that session’s worktree. Otherwise it’s the project’s repository, and a run with neither uses the folder Valdr was started from. If the session has no worktree, the step is blocked before your program starts. A step can override the default with cwd, and relative values resolve from it. Read the current directory with your language’s standard library; Valdr doesn’t pass it in the request.
Keep tools reusable: don’t hard-code a project key or a local repository path in the manifest.
Environment. Programs inherit PATH, HOME, TMPDIR, LANG, and LC_* variables. To pass more, list the variable names in process.inheritEnv and make their values available on the workflow host. Tools don’t read your browser’s environment.
Limits. The effective limit is the smaller of your manifest’s value and the host maximum:
| Limit | Host maximum | When it’s exceeded |
|---|---|---|
| Run time | 300 seconds | Valdr attempts to stop the tool and reports a timeout |
| Request size, including Valdr’s fields | 1 MiB | The step fails before your program starts |
| Result on stdout | 1 MiB | Valdr attempts to stop the tool and rejects the result |
| Saved stderr | 1 MiB | The log is not retained, so keep diagnostics short |
Stopping a tool cannot undo an external change or guarantee that detached work has stopped. Check what happened before repeating an action.
Environment variables
Use ~/.valdr/environment.env on the machine running Valdr to supply the same environment references to workflow user tools started through the UI or MCP. Create the .valdr directory if needed, then add one reference per line:
MY_API_TOKEN=${MY_API_TOKEN}
SOME_VAR=$(printf '%s' hello)${MY_API_TOKEN} reads the variable from your interactive login shell; define it in your shell startup configuration. $(command) reads the output of a trusted local command, such as a secret-manager lookup. The second line supplies the non-secret value hello.
The file accepts whole-value ${NAME} or $(command) references, with blank lines and # comment lines. Literal assignments such as SOME_VAR=hello aren’t supported. Keep token values out of this file, manifests, and workflow inputs.
Each tool must still list the names it needs in process.inheritEnv. For example, a Node tool’s process section could be:
process:
executable: node
args: [main.mjs]
inheritEnv: [MY_API_TOKEN, SOME_VAR]An explicit variable in the environment used to launch the UI or MCP takes precedence, including an empty value. The file is optional; without it, tools can still receive declared variables from the launch environment.
References resolve once per UI or MCP process. Restart Valdr and your MCP hosts after changing the file or its source values, including after removing a reference. If Valdr reports an invalid or unresolved reference, fix the file, shell variable, or command and restart.
Redaction and sensitive output
Redaction is best effort. Valdr attempts to remove secrets from recorded inputs, stderr, and error messages, but it may miss sensitive values. You are responsible for what your tool returns and logs, including output from any CLI or API it calls.
Successful data is saved in run history and passed to later steps without Valdr redaction. Return only the fields your workflow needs, and remove credentials and unnecessary sensitive data before writing results, errors, or logs. Keep secrets out of workflow inputs and manifests; use the host environment or the CLI’s own sign-in.
Design retries for real effects
The sideEffects and retry fields are promises to the people running your tool. Make them accurate:
| Your action… | Declare | Why |
|---|---|---|
| Only reads | sideEffects: read, retry: idempotent | Repeating it is always safe |
Writes, and the destination can deduplicate on operationId | sideEffects: write, retry: idempotent | A retry of the same operation won’t write twice |
Writes that converge, such as a build into target/ | sideEffects: write, retry: idempotent | Running it again overwrites the same outputs |
| Writes without reliable deduplication | sideEffects: write, retry: manual | A person should check before repeating |
| Has effects you can’t establish | sideEffects: unknown, retry: manual | Treat it as a write |
What Valdr does with those declarations:
- If a manual-retry or unknown-effect action fails after your program starts, even with a handled error, the step is blocked with a needs attention failure and Retry step isn’t offered. This includes a timeout or cancellation, which doesn’t prove a write didn’t happen. Failures Valdr catches before your program starts, such as invalid input or a missing executable, can still be retried.
- An idempotent action that fails or times out can be retried.
- If Valdr stops mid-attempt, for example because the host restarts, and can’t confirm the outcome, the step also needs attention, even for an idempotent action.
- If Valdr restarts after your program succeeded, it uses the saved result instead of running your program again.
- If Valdr cannot reuse the original inputs for recovery, the step needs attention. Check what already happened before starting a new run.
Inside your adapter:
- Use
operationIdfor deduplication, neverattemptId, which changes on every retry. - Don’t add automatic retry loops around a write whose outcome is uncertain.
- Don’t rely on in-memory state between attempts.
- If users need to undo a completed effect, give them a separate action that reverses it.
Next step
Test success and failure behavior before you share your tool.