Key points
- Both write routes take the YAML document as the request body. There is no JSON wrapper, and sending one produces a parse error that reads like a schema error.
- Validation always answers 200: a rejected document is
valid: falsewith issues carrying a path, a line, and a code. - Pipeline and step identifiers are catch-all path segments, so a team-prefixed name with slashes is passed through as-is.
- A 403 rather than a 404 tells you the definition exists and access is the problem.
- Editing a GitOps-managed definition creates a database override, which then shows as drift until it is pushed or discarded.
- Deleting a pipeline keeps its runs; deleting a reusable step breaks the pipelines that include it, and nothing stops you.
Operations
GET/v1/pipelinesAuthorized
Lists pipelines the caller can see.
Notes
An empty list can mean "no pipelines" or "none you may see". Compare with /v1/auth/me before assuming the former.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
include_source | query | boolean | Optional | Include where each pipeline came from, which distinguishes a GitOps-managed definition from a database one.Default: false |
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/pipelines?include_source=true" | jqResponses
Pipelines ordered by path then name. The list is filtered per caller, so two callers can legitimately see different rows.
[
{
"id": "platform/release-service",
"source": "config_repo",
"version": "latest",
"updated_at": "2026-08-19T09:58:12Z"
}
]When it fails
| Status | Cause | What to do |
|---|---|---|
| 500 | The pipeline query failed. | Platform fault; check database reachability. |
| 503 | Authorization is unavailable, so the list cannot be filtered safely. | Check AAA. The platform refuses to answer rather than return an unfiltered list. |
Side effects
- None.
Proven by
services/nopsai/pipeline_handlers_test.goservices/nopsai/pipeline_handlers.go
GET/v1/pipelines/{pipelineName...}Authorized
Reads one pipeline definition.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
pipelineName | path | string | Required | Pipeline identifier. It is a catch-all segment, so a team-prefixed name with slashes is passed as-is. |
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/pipelines/platform/release-service" | jq -r .definitionResponses
The definition and its provenance.
{
"id": "platform/release-service",
"source": "config_repo",
"version": "latest",
"definition": "name: release-service\ncontainer_image: alpine:3.20\n..."
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | The name is empty or malformed. | Use the identifier from the list route. |
| 403 | The caller may not read this pipeline. | A 403 rather than a 404 means the pipeline exists and access is the problem. |
| 404 | No pipeline with that identifier. | Check the team prefix: release-service and platform/release-service are different pipelines. |
| 502 | A GitOps-backed definition could not be fetched from its repository. | Check the configuration repository connection and sync status. |
| 503 | Authorization is unavailable. | Check AAA. |
| 500 | The definition could not be read. | Platform fault. |
Side effects
- None.
Proven by
services/nopsai/pipeline_handlers_test.goservices/nopsai/pipeline_handlers.go
PUT/v1/pipelines/{pipelineName...}Authorized
Creates or updates a pipeline.
Notes
The request body is the YAML document, not {"definition": "..."}. Sending JSON gets a parse failure that looks like a schema error.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
pipelineName | path | string | Required | Identifier to store the definition under. |
Call it
curl -sX PUT "$NOPSAI_URL/v1/pipelines/platform/release-service" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/yaml" \
--data-binary @release-service.yamlResponses
Stored. The same status is returned for a create and an update.
{
"id": "platform/release-service",
"source": "database",
"version": "latest",
"updated_at": "2026-08-19T10:22:31Z"
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | The YAML is malformed, or the pipeline fails validation. | Run POST /v1/pipelines/validate first: it returns every issue with a path and a line instead of the first failure. |
| 500 | The definition could not be persisted. | Retry; nothing was stored. |
Side effects
- Editing a GitOps-managed pipeline creates a database override, which then shows as drift against Git until it is pushed or discarded.
- Writes an audit record.
Proven by
services/nopsai/pipeline_handlers_test.goservices/nopsai/pipeline_handlers.go
DELETE/v1/pipelines/{pipelineName...}Authorized
Deletes a pipeline.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
pipelineName | path | string | Required | Identifier of the pipeline to delete. |
Call it
curl -sX DELETE -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/pipelines/platform/release-service" -w "%{http_code}\n"Responses
Deleted.
When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | The name is empty or malformed. | Use the identifier from the list route. |
| 500 | The delete could not be persisted. | Retry. |
Side effects
- Removes the definition. Runs it already produced stay, so history survives the pipeline.
- A GitOps-managed pipeline reappears on the next sync unless it is removed from the repository too.
- Writes an audit record.
Proven by
services/nopsai/pipeline_handlers_test.goservices/nopsai/pipeline_handlers.go
POST/v1/pipelines/validateAuthenticated
Validates pipeline YAML without saving it.
Notes
Always check valid rather than the status code. A rejected pipeline returns 200 with valid: false.
Call it
curl -sX POST "$NOPSAI_URL/v1/pipelines/validate" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/yaml" \
--data-binary @release-service.yaml | jqResponses
The document is valid.
{
"valid": true,
"errors": [],
"warnings": []
}The document is not valid. Validation failures are a body, not an HTTP error — the request itself succeeded.
{
"valid": false,
"errors": [
{
"message": "step \"package\" consumes an output of \"build\" without a dependency path",
"path": "steps[3].variables.BUILD_TAG",
"line": 34,
"code": "missing_dependency_path"
}
],
"warnings": []
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | The request payload could not be read at all — invalid JSON when a JSON content type was declared. | Send YAML with a YAML content type, or JSON with the document under yaml or content. |
Side effects
- None. Validation never stores anything.
Proven by
services/nopsai/pipeline_handlers_test.goservices/nopsai/validation_handlers.goservices/nopsai/validation_contract.go
GET/v1/stepsAuthorized
Lists reusable step definitions.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
include_source | query | boolean | Optional | Include where each step definition came from.Default: false |
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/steps" | jqResponses
Reusable steps the caller may see.
[
{
"id": "platform/shared/checkout",
"source": "config_repo",
"updated_at": "2026-08-18T16:40:02Z"
}
]When it fails
| Status | Cause | What to do |
|---|---|---|
| 500 | The step query failed. | Platform fault. |
| 503 | Authorization is unavailable. | Check AAA; the list is not returned unfiltered. |
Side effects
- None.
Proven by
services/nopsai/pipeline_handlers_test.goservices/nopsai/pipeline_handlers.go
GET/v1/steps/{stepPath...}Authorized
Reads one reusable step.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
stepPath | path | string | Required | Step identifier, including its team prefix. |
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/steps/platform/shared/checkout" | jq -r .definitionResponses
The step definition and its provenance.
{
"id": "platform/shared/checkout",
"source": "config_repo",
"definition": "name: checkout\nscript: |\n git clone ...\n"
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 404 | No reusable step with that identifier. | List the steps: an include: that names a missing step fails pipeline validation with the same identifier. |
| 403 | The caller may not read this step. | Check ownership of the team that holds it. |
Side effects
- None.
Proven by
services/nopsai/pipeline_handlers_test.goservices/nopsai/pipeline_handlers.go
PUT/v1/steps/{stepName...}Authorized
Creates or updates a reusable step.
Notes
A shared step is shared blast radius: it takes effect for every pipeline that includes it, without those pipelines changing.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
stepName | path | string | Required | Identifier to store the step under. |
Call it
curl -sX PUT "$NOPSAI_URL/v1/steps/platform/shared/checkout" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/yaml" \
--data-binary @checkout.yamlResponses
Stored, for both create and update.
{
"id": "platform/shared/checkout",
"source": "database",
"updated_at": "2026-08-19T10:31:44Z"
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | The YAML is malformed or the step fails validation. | Validate first: a reusable step has its own rules and rejects pipeline-only directives. |
| 500 | The definition could not be persisted. | Retry. |
Side effects
- Every pipeline that includes this step picks up the change on its next run.
- Writes an audit record.
Proven by
services/nopsai/pipeline_handlers_test.goservices/nopsai/pipeline_handlers.go
DELETE/v1/steps/{stepName...}Authorized
Deletes a reusable step.
Notes
Nothing blocks deleting a step other pipelines include. Check usage before removing a shared definition.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
stepName | path | string | Required | Identifier of the step to delete. |
Call it
curl -sX DELETE -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/steps/platform/shared/checkout" -w "%{http_code}\n"Responses
Deleted.
When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | The name is empty or malformed. | Use the identifier from the list route. |
| 500 | The delete could not be persisted. | Retry. |
Side effects
- Pipelines that still include the step fail validation on their next save or run.
- Writes an audit record.
Proven by
services/nopsai/pipeline_handlers_test.goservices/nopsai/pipeline_handlers.go
POST/v1/steps/validateAuthenticated
Validates a reusable step definition.
Notes
A reusable step is validated against step rules, so a document that passes here can still be rejected as a pipeline, and the reverse.
Call it
curl -sX POST "$NOPSAI_URL/v1/steps/validate" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/yaml" \
--data-binary @checkout.yaml | jqResponses
Validation ran. Read valid rather than the status code.
{
"valid": true,
"errors": [],
"warnings": []
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | The payload could not be read. | Send YAML with a YAML content type, or JSON with the document under yaml or content. |
Side effects
- None.
Proven by
services/nopsai/pipeline_handlers_test.goservices/nopsai/validation_handlers.go
How it works
Validate before saving, always. Save rejects on the first failure with a 400, while validation returns every issue at once with the line that produced it — the difference between one round trip and five.
A list route returning fewer rows than expected is usually authorization rather than absence: the result is filtered per caller. If authorization itself is unavailable, the platform answers 503 rather than returning an unfiltered list.
Reusable steps and pipelines are validated against different rule sets. A document that validates as a step can be rejected as a pipeline, and the reverse, so use the matching validation route.
Implementation evidence
services/nopsai/pipeline_handlers.goPipeline and reusable step handlers.
services/nopsai/validation_contract.goThe valid/errors/warnings response shape.

