Platform

Platform overviewArchitectureWorkflow orchestrationGitOps configurationGovernance and AAAAI and MCPKnowledge and contextRuntime and executionEvidence and monitoring

Use cases

All use casesProduction incidentRelease preparationHotfix to productionSecurity scan triage
Why NopsAIIntegrationsSecurity

Resources

All resourcesAI agent governanceMCP governanceMCP securitySelf-hosted platforms
PricingGitHub

Company

How a run worksAboutContactBook a demo

External triggers API

Defining an entry point another system may call, invoking it safely, and diagnosing a caller that believes it is being ignored.

ReferenceDeveloperAutomation authorSecurity

Key points

  • Invoking answers 202 when it queues a run, and 200 when a replayed idempotency key returns the original run.
  • The idempotency key is scoped by trigger and caller, so two systems using the same key do not collide.
  • A key currently in flight answers 409 rather than starting a second run.
  • rate_limit.per_minute is enforced per trigger and answers 429 when exceeded.
  • payload_schema is checked before a run is created, so a malformed call costs nothing.
  • Every call, accepted or rejected, is recorded in the invocation history with its caller and reason.
  • The JSON body and the GitOps YAML document carry the same fields, so a trigger can move between the two unchanged.

Operations

GET/v1/external-triggersAuthorized

Lists external triggers the caller can see.

Call it

List external triggersapi-external-triggers request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/external-triggers" | jq '.[] | {id, name, pipeline, enabled}'
Result

One entry per trigger, filtered to what the caller may see.

Responses

200application/json

Triggers visible to the caller.

[{
  "id": "3d2b1f88-77aa-4c19-9f3d-52b0c4e7a901",
  "name": "start-first-pipeline",
  "enabled": true,
  "pipeline": "platform/release-service",
  "scope": "platform/production",
  "run_team_path": "platform/payments",
  "allowed_callers": [{ "service_account": "release-bot" }],
  "variable_mapping": { "RELEASE_CHANNEL": "payload.channel" },
  "rate_limit": { "per_minute": 10 }
}]

When it fails

StatusCauseWhat to do
405A method other than GET.Use GET.
503Authorization is unavailable, so the list cannot be filtered.Check AAA.
500The query failed.Platform fault.

Side effects

  • None.

Proven by

  • services/nopsai/external_triggers_test.go
  • services/nopsai/external_triggers.go
POST/v1/external-triggersAuthorized

Creates an external trigger.

Notes

The JSON body and the GitOps YAML document carry the same fields, so a trigger created here can be exported into a configuration repository unchanged.

Call it

Create a triggerapi-external-triggers request
curl -sX POST "$NOPSAI_URL/v1/external-triggers" \
  -H "Authorization: Bearer $NOPSAI_TOKEN" \
  -H "Content-Type: application/json" \
  --data @trigger.json | jq -r .id
Result

The created trigger, including the id used to invoke it.

Replace before running
  • trigger.json mirrors the GitOps document: name, pipeline, scope, run_team_path, allowed_callers, variable_mapping, payload_schema, and rate_limit.

Responses

201application/json

Trigger created.

{
  "id": "3d2b1f88-77aa-4c19-9f3d-52b0c4e7a901",
  "name": "start-first-pipeline",
  "enabled": true,
  "pipeline": "platform/release-service",
  "scope": "platform/production",
  "run_team_path": "platform/payments",
  "allowed_callers": [{ "service_account": "release-bot" }],
  "variable_mapping": { "RELEASE_CHANNEL": "payload.channel" },
  "rate_limit": { "per_minute": 10 }
}

When it fails

StatusCauseWhat to do
400The document is invalid: a missing pipeline, an unusable caller entry, or a malformed mapping.The message names the field that failed normalisation.
401The caller identity could not be resolved for the created-by record.Use a user or service account token, not an anonymous request.
405A method other than POST.Use POST.
500The trigger could not be stored.Retry.

Side effects

  • Creates an entry point that can start runs.
  • Records who created it.
  • Writes an audit record.

Proven by

  • services/nopsai/external_triggers_test.go
  • services/nopsai/external_triggers_schema_test.go
  • services/nopsai/external_triggers.go
  • services/nopsai/external_triggers_gitops.go
GET/v1/external-triggers/{id}Authorized

Reads one external trigger.

Parameters

NameInTypeRequiredDescription
idpathuuidRequiredTrigger identifier.

Call it

Read a triggerapi-external-triggers request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/external-triggers/$TRIGGER_ID" | jq
Result

The full trigger document, including its caller list and payload contract.

Responses

200application/json

The trigger.

{
  "id": "3d2b1f88-77aa-4c19-9f3d-52b0c4e7a901",
  "name": "start-first-pipeline",
  "enabled": true,
  "pipeline": "platform/release-service",
  "scope": "platform/production",
  "run_team_path": "platform/payments",
  "allowed_callers": [{ "service_account": "release-bot" }],
  "variable_mapping": { "RELEASE_CHANNEL": "payload.channel" },
  "rate_limit": { "per_minute": 10 }
}

When it fails

StatusCauseWhat to do
404No trigger with that id.Confirm the id from the list route.
405A method other than GET.Use GET.
500The trigger could not be loaded.Platform fault.

Side effects

  • None.

Proven by

  • services/nopsai/external_triggers_test.go
  • services/nopsai/external_triggers.go
PUT/v1/external-triggers/{id}Authorized

Replaces an external trigger definition.

Notes

Disabling with enabled: false is safer than deleting while you investigate a misbehaving caller: the invocation history stays attached.

Parameters

NameInTypeRequiredDescription
idpathuuidRequiredTrigger to replace.

Call it

Replace a triggerapi-external-triggers request
curl -sX PUT "$NOPSAI_URL/v1/external-triggers/$TRIGGER_ID" \
  -H "Authorization: Bearer $NOPSAI_TOKEN" \
  -H "Content-Type: application/json" \
  --data @trigger.json | jq
Result

The stored trigger after the replacement.

Replace before running
  • A PUT replaces the whole document. Omitted fields are cleared, not kept.

Responses

200application/json

Trigger replaced.

{
  "id": "3d2b1f88-77aa-4c19-9f3d-52b0c4e7a901",
  "name": "start-first-pipeline",
  "enabled": true,
  "pipeline": "platform/release-service",
  "scope": "platform/production",
  "run_team_path": "platform/payments",
  "allowed_callers": [{ "service_account": "release-bot" }],
  "variable_mapping": { "RELEASE_CHANNEL": "payload.channel" },
  "rate_limit": { "per_minute": 10 }
}

When it fails

StatusCauseWhat to do
400The document is invalid.The message names the field.
404No trigger with that id.Create it instead.
405A method other than PUT or PATCH.Use PUT to replace or PATCH to merge.
500The update could not be persisted.Retry.

Side effects

  • Changes what callers may invoke and what the trigger starts.
  • Writes an audit record.

Proven by

  • services/nopsai/external_triggers_test.go
  • services/nopsai/external_triggers.go
PATCH/v1/external-triggers/{id}Authorized

Partially updates an external trigger.

Parameters

NameInTypeRequiredDescription
idpathuuidRequiredTrigger to update.

Call it

Disable a trigger without changing anything elseapi-external-triggers request
curl -sX PATCH "$NOPSAI_URL/v1/external-triggers/$TRIGGER_ID" \
  -H "Authorization: Bearer $NOPSAI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled":false}' | jq '{id, enabled}'
Result

The trigger with enabled: false; every other field keeps its stored value.

Responses

200application/json

Trigger updated.

{
  "id": "3d2b1f88-77aa-4c19-9f3d-52b0c4e7a901",
  "name": "start-first-pipeline",
  "enabled": true,
  "pipeline": "platform/release-service",
  "scope": "platform/production",
  "run_team_path": "platform/payments",
  "allowed_callers": [{ "service_account": "release-bot" }],
  "variable_mapping": { "RELEASE_CHANNEL": "payload.channel" },
  "rate_limit": { "per_minute": 10 }
}

When it fails

StatusCauseWhat to do
400A supplied field is invalid.The message names the field.
404No trigger with that id.Confirm the id.
500The update could not be persisted.Retry.

Side effects

  • Writes an audit record.

Proven by

  • services/nopsai/external_triggers_test.go
  • services/nopsai/external_triggers.go
DELETE/v1/external-triggers/{id}Authorized

Deletes an external trigger.

Parameters

NameInTypeRequiredDescription
idpathuuidRequiredTrigger to delete.

Call it

Delete a triggerapi-external-triggers request
curl -sX DELETE -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/external-triggers/$TRIGGER_ID" -w "%{http_code}\n"
Result

204. Callers holding the id start receiving 404.

Responses

204

Deleted.

When it fails

StatusCauseWhat to do
404No trigger with that id.It may already be deleted.
405A method other than DELETE.Use DELETE.
500The delete could not be persisted.Retry.

Side effects

  • Removes the entry point. Runs it already started are unaffected.
  • Writes an audit record.

Proven by

  • services/nopsai/external_triggers_test.go
  • services/nopsai/external_triggers.go
POST/v1/external-triggers/{id}/invokeAuthorized

Starts a run through the trigger.

Notes

The idempotency key is scoped by trigger and caller. Two systems using the same key against the same trigger do not collide.

Parameters

NameInTypeRequiredDescription
idpathuuidRequiredTrigger to invoke.

Call it

Invoke with an idempotency keyapi-external-triggers request
curl -sX POST "$NOPSAI_URL/v1/external-triggers/$TRIGGER_ID/invoke" \
  -H "Authorization: Bearer $CALLER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"idempotency_key":"change-4821","payload":{"channel":"stable"}}' | jq
Result

202 with the run id on the first call; 200 with the same run id when the key is replayed.

Replace before running
  • $CALLER_TOKEN must belong to an identity named in allowed_callers.

Responses

202application/json

Accepted: a run was queued from this invocation.

{
  "run_id": "9c1f7a5e-2b44-4d2f-8f2a-2c9f0b6d4e11",
  "trigger_event_id": "b0f1...",
  "status": "queued"
}
200application/json

The idempotency key was already used and its run is returned instead of starting a second one.

{
  "run_id": "9c1f7a5e-2b44-4d2f-8f2a-2c9f0b6d4e11",
  "trigger_event_id": "b0f1...",
  "status": "queued"
}

When it fails

StatusCauseWhat to do
400The payload fails payload_schema, or the body is malformed.The schema is checked before a run is created, so a rejected payload costs nothing.
401No usable caller identity on the request.Send a token for an identity in allowed_callers.
403The caller is authenticated but not permitted to invoke this trigger.allowed_callers narrows an already-authorized set; AAA must also allow the caller on the trigger resource.
404No trigger with that id, or it is disabled.Check enabled on the trigger.
409The same idempotency key is currently being processed.Wait and retry: the first call is still in flight.
429rate_limit.per_minute was exceeded over the previous minute.Back off. The limit is per trigger, not per caller.
503Authorization is unavailable.Check AAA; the invocation is refused rather than run unauthorized.
500The run could not be created.Retry with the same idempotency key.

Side effects

  • Records an invocation, accepted or rejected, with the caller and the reason.
  • Creates a run and maps payload values into run variables through variable_mapping.
  • Updates last_used_at on the trigger.

Proven by

  • services/nopsai/external_triggers_test.go
  • services/nopsai/external_triggers.go
GET/v1/external-triggers/{id}/invocationsAuthorized

Lists invocation history for a trigger.

Notes

This is the fastest answer to "my system says it is triggering runs and nothing happens": a rejected call is recorded here with its reason.

Parameters

NameInTypeRequiredDescription
idpathuuidRequiredTrigger identifier.
limitqueryintegerOptionalPage size for the history.

Call it

Read invocation historyapi-external-triggers request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/external-triggers/$TRIGGER_ID/invocations" | jq
Result

Accepted and rejected calls alike, each with its caller and outcome.

Responses

200application/json

Invocation records, newest first.

[
  {
    "id": "6a3c...",
    "trigger_id": "3d2b1f88-77aa-4c19-9f3d-52b0c4e7a901",
    "caller_type": "service_account",
    "caller_id": "release-bot",
    "status": "queued",
    "run_id": "9c1f7a5e-2b44-4d2f-8f2a-2c9f0b6d4e11"
  }
]

When it fails

StatusCauseWhat to do
405A method other than GET.Use GET.
500The history query failed.Platform fault.

Side effects

  • None.

Proven by

  • services/nopsai/external_triggers_test.go
  • services/nopsai/external_triggers.go

How it works

Read the status codes as a state machine: 202 means this call created work, 200 means an earlier identical call did, 409 means one is happening right now, and 429 means you are asking too often. A client that treats all four as success will double-run; one that treats them all as failure will retry forever.

allowed_callers narrows rather than grants. AAA still authorizes the caller against the trigger resource first, which is why an empty list does not open the trigger to everyone.

When a caller insists it is triggering runs and nothing appears, read the invocation history before anything else — a 403 or a schema rejection is recorded there with the reason.

Implementation evidence

  • services/nopsai/external_triggers.go

    Invocation, idempotency, and rate-limit handling.

  • services/nopsai/external_triggers_gitops.go

    The trigger document schema shared with GitOps.