# Decisions
> Make structured decisions over an input state document with step.decision, with validated typed answers from the built-in jevos provider.
Some steps cannot be expressed as a shell command or a transform. Deciding
whether a finding is in scope, ranking leads, or choosing the next action needs
judgement over messy input. `step.decision` makes that judgement an explicit,
typed step: it declares an input state, one or more named questions, a provider,
and where the validated result is written.
## `step.decision`
```bhw
step.decision(
state = path.file("findings.json"),
questions = {
"relevant": decision.noul("Is this finding relevant?"),
"severity": decision.choice("Severity?", options = ["low", "medium", "high"]),
"priority": decision.score("Priority?", min = 0, max = 3),
},
provider = "jevos",
endpoint = config.var("jevos_endpoint"),
output = path.file("decision.json"),
)
```
|
Parameter
|
Meaning
|
state
|
Input document the decision is made over (
path.file(...)
), read at execution time and treated as untrusted data
|
questions
|
Ordered
{name: decision_question}
mapping; names are unique
|
provider
|
Optional decision provider:
"jevos"
(the default) for the built-in live server, or
"mock"
for the deterministic in-process provider
|
output
|
Where the validated result document is written (
path.file(...)
)
|
endpoint
|
jevos endpoint, often
config.var(...)
(default local server:
http://127.0.0.1:8017/v1/systemone
)
|
api_key
|
Optional jevos API key; must be a
config.secret(...)
reference
|
model
|
Optional jevos model alias (default
jev-latest
)
|
## Question types
Questions are created with the `decision` namespace and carry their own
validation rules:
```bhw
decision.noul("Is this finding relevant?")
decision.choice("Severity?", options = ["low", "medium", "high"])
decision.score("Priority?", min = 0, max = 3)
```
- `noul` is binary: jevos returns P(yes), the answer is `P(yes) >= 0.5`, and
the probability is carried through as the reported confidence.
- `choice` requires a non-empty list of unique, non-empty options; the answer
must be one of them.
- `score` requires ordered bounds (`min` must not exceed `max`); the answer must
be an integer inside them.
Invalid definitions are rejected when the workflow is parsed. Invalid provider
answers are rejected at execution time, before anything is written.
## Inputs and credentials
`state` is a file and is always treated as data, never as instructions. Provider
configuration is referenced, never embedded:
```bhw
step.decision(
state = path.file("findings.json"),
questions = {"relevant": decision.noul("Is this finding relevant?")},
provider = "jevos",
endpoint = config.var("jevos_endpoint"),
api_key = config.secret("jevos_api_key"),
output = path.file("decision.json"),
)
```
`api_key` accepts only a `config.secret(...)` reference; literal credentials are
rejected so they can never enter the serialized workflow definition. Endpoints
and keys are resolved at execution time and are not written to the result
document.
## Providers
`provider` is optional and selects the decision implementation; it defaults to
`"jevos"`. Two providers ship, and the platform standardizes on them:
- `"jevos"` is the built-in live provider: a self-hosted
[jevos](https://github.com/feder-cr/jev) decision server, reached over HTTP at
`endpoint` (default `http://127.0.0.1:8017/v1/systemone`). jevos answers
yes/no (`noul`), multiple-choice, and score questions on a laptop CPU and
returns a probability for each answer. Point `endpoint` at your jevos instance
and, if you set `JEV_API_KEY` on it, supply the same key through
`config.secret(...)`.
- `"mock"` is deterministic and in-process. It needs no endpoint or credential
and is what the test suite uses, so decision steps stay hermetic.
Adding a provider is a code change, not a configuration value: the engine
rejects any other `provider` name rather than implying a provider zoo.
Two jevos limits surface as parse-time or step errors: a `choice` needs 2–26
options and a `score` needs 2–10 levels, so keep score bounds within a ten-point
span.
## The result document
The validated result is written to `output` as JSON: a mapping from question
name to a typed answer.
```json
{
"answers": {
"relevant": {"type": "noul", "value": true, "probability": 0.82},
"severity": {"type": "choice", "value": "high"},
"priority": {"type": "score", "value": 2}
},
"order": ["relevant", "severity", "priority"]
}
```
Downstream steps consume the file like any other workspace artifact:
```bhw
step.exec(command = ["jq", "-r", ".answers.severity.value", "decision.json"])
```
## Trust boundary
A decision result is model-produced data, not proof. Every answer is validated
against the question that produced it before the document is written, but:
- a self-reported `probability` is not a calibrated probability, and
- a `choice` or `score` answer is a judgement about the input, not a verified
fact.
Treat the document as one more input to validate downstream, not as an
authority. Because the questions and their bounds are declared in the workflow,
the result stays typed and reviewable instead of collapsing into free text.
## A security-triage workflow
```bhw
triage = activity(
name = "triage",
triggers = [trigger.dispatch()],
steps = [
step.exec(command = ["nuclei", "-l", "live.txt", "-jsonl", "-o", "findings.json"]),
step.decision(
state = path.file("findings.json"),
questions = {
"relevant": decision.noul("Is this finding relevant to the target?"),
"severity": decision.choice(
"How severe is the finding?",
options = ["low", "medium", "high", "critical"],
),
"priority": decision.score("Triage priority?", min = 0, max = 3),
},
provider = "jevos",
endpoint = config.var("jevos_endpoint"),
output = path.file("decision.json"),
),
step.exec(command = ["sh", "-c", "jq -r '.answers.priority.value' decision.json > priority.txt"]),
],
)
workflow(name = "triage", activities = [triage])
```