External Push
The push-driven KRI source for Draxis. When your data lives behind custom logic, a Jira JQL counting tickets by age, a GitHub Actions build-failure rate, a Lambda that crunches a regional API, External Push gives you a Draxis-owned source row plus two MCP tools so an external scheduler can record the resulting number directly. Source-scoped credentials mean one push token per job, scoped to one KRI source, with no read access to the rest of your tenant.
At a glance
| Vendor | Draxis (first-party). External Push is built into the platform, there is no third-party vendor to authorize against. |
|---|---|
| Source type | external_push |
| Vendor ID (slug) | external-push |
| Base URL | n/a, push-driven. The Add Integration form hides the API Endpoint, Authentication Method, and Schedule fields for this vendor and renders an inline helper card instead. |
| Auth method | Source-scoped Personal Access Token (PAT) minted from the integration's card after save. The PAT is restricted to one kri_source.id, calling any other tool with it returns a scope-mismatch error. |
| Schedule default | manual, External Push never runs on a cron. Values land when your external scheduler calls push_kri_value. |
| What it produces | KRI rows under this source whose value, kri_status, and last_updated reflect the most recent push (or the historical timestamp you pass). |
| What it costs | Nothing beyond the bandwidth of the MCP call. No LLM, no inference, no extra storage tier. |
| Availability | New in 2026.05. |
What problem this solves
Draxis enforces a strict invariant: every KRI must trace back to a real kri_source. That keeps the dashboard honest, a row nothing ever updates is dead data, and the system surfaces the staleness instead of hiding it. The three legitimate creation paths today are vendor connectors (Okta, CrowdStrike, etc.), GRC pulls (Vanta), and the AI Drop Zone for unstructured artifacts.
That covers most of the surface, but it leaves a real gap: metrics where you have custom data-fetching logic and just want Draxis to record the resulting number on a schedule. The AI Drop Zone can handle this via its webhook, but routing a number you already know through the LLM extractor is overhead. The MCP Client connector covers it if you're willing to host your own MCP server. Neither fits the "I have a small cron job, just take this number" case.
External Push is the fourth path. The admin creates a source from Settings → Integrations and mints a push token scoped to that source. The external scheduler calls create_external_push_kri once (idempotent, safe to call every run) and push_kri_value on every run. The KRI row still references a real kri_source.id, so the invariant holds; dashboard aggregations, breach pies, history charts, and audit logs all work unchanged.
The two MCP tools
create_external_push_kri
Idempotent UPSERT of a KRI under your external_push source. The KRI id is deterministic on (sourceId, name), so re-calling with the same arguments matches the existing row and returns created: false. Safe to call on every cron run.
{
"sourceId": "src-extpush-1747...", // required
"name": "Build Failure Rate", // required
"measurementType": "percentage", // count | percentage | ratio | duration | currency | score | level | boolean
"unit": "%", // display-only
"thresholdYellow": 5, // optional
"thresholdRed": 10, // optional
"higherIsBetter": false // default false
}
Thresholds are optional. Omit them on the first run and the KRI starts “untuned”, the value flows through, but the green / yellow / red status is meaningless until an admin tunes the bands in Settings → KRIs. This is the right default for cron-job authors: encoding business policy (“what counts as bad?”) in your script is usually the wrong call, that’s a risk-owner decision.
push_kri_value
Record an observation. Errors with an actionable message if the KRI doesn’t exist (“call create_external_push_kri first”), so a typo in name becomes a clear error instead of a silently-spawned duplicate.
{
"sourceId": "src-extpush-1747...", // required
"name": "Build Failure Rate", // required, same slug as create
"value": 3.2, // required, for boolean KRIs pass 1 or 0
"timestamp": "2026-04-01T00:00:00Z" // optional, ISO-8601, defaults to now
}
In one transaction the tool: recomputes the KRI’s green/yellow/red status from its current thresholds, sets kri.value and kri.last_updated, advances kri_source.last_run_at + last_run_status='ok' (so the source counts as “live” in dashboard rollups), and writes an integration_run audit row that surfaces in the Integrations UI just like a scheduled connector run.
Historical backfill is first-class
The optional timestamp field is real backfill, not a cosmetic field. The KRI’s value-history trigger honors the timestamp you pass, so a backfilled point lands at its true position in the kri_value_history series. The history chart, sparkline, and trend math all reflect the right ordering.
Use cases:
- Initial onboarding. Push the last 30 days of values one-shot so the trend chart isn’t a single dot on day one.
- Catch-up after an outage. Your scheduler missed two days, push the two missed timestamps after recovery and the series fills in cleanly.
- Reconciliation with a system of record. If you discover the source data was wrong for a past day, push a corrected value at the historical timestamp.
For ongoing measurements just omit timestamp, the server stamps the moment of the push.
Source-scoped tokens (per-job credentials)
A push token issued from the integration’s card carries a scope_to_source_id claim. The MCP server enforces it: such a token can only call create_external_push_kri and push_kri_value, and only against the source it’s bound to. Every other tool, list_risks, list_kris, get_program_disclosure, even submit_dropzone_artifact, refuses the token with a clear scope-mismatch error.
This is the principle of least privilege applied at the token boundary: one credential per cron job, blast radius bounded to one KRI source. If a push token ends up in your scheduler’s logs or a leaked CI dump, the worst an attacker can do is overwrite the values on KRIs under that one source. They cannot pivot to read your risk register or vendor data.
For ad-hoc debugging from a tenant-wide PAT, the push tools still work, the scope check only kicks in when the PAT itself is source-scoped. Admins don’t need to mint a second token to test from their main account.
Wire it into Draxis
- Open Settings → Integrations in your tenant.
- Click Add integration and pick External Push from the source type dropdown. The vendor auto-selects External Push (cron / CI / custom script).
- Name the integration after the system that’ll push to it (e.g. “Jira open critical bugs”, “GitHub Actions build health”, “Datadog SLO budget”). The form hides API Endpoint, Authentication, and Schedule, External Push doesn’t use any of them.
- Save. The integration appears in the list with an External Push panel attached.
- In the panel, click Mint Push Token, name the token after the job that’ll use it, and copy the plaintext immediately (it’s shown exactly once).
- Paste the token into your scheduler as a secret (GitHub Actions secret, Lambda env var, etc.).
- Author the script: call
create_external_push_krionce at the top, thenpush_kri_valueon every run. The panel ships with curl examples that havesourceIdalready substituted, copy & adapt.
Example: GitHub Actions, build-failure-rate KRI
One-shot workflow that computes the last week’s build-failure rate via the GitHub REST API and pushes it to Draxis:
name: Push build failure rate to Draxis
on:
schedule: [{ cron: '0 6 * * *' }] # daily at 06:00 UTC
workflow_dispatch:
jobs:
push:
runs-on: ubuntu-latest
steps:
- name: Compute + push
env:
DRAXIS_PAT: ${{ secrets.DRAXIS_PUSH_TOKEN }}
DRAXIS_SOURCE: ${{ secrets.DRAXIS_SOURCE_ID }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
# Pull last 7 days of workflow runs on this repo
RUNS=$(gh api -X GET "repos/${{ github.repository }}/actions/runs" \
-f per_page=100 -f created=">$(date -d '7 days ago' +%Y-%m-%d)")
TOTAL=$(echo "$RUNS" | jq '.total_count')
FAILED=$(echo "$RUNS" | jq '[.workflow_runs[] | select(.conclusion == "failure")] | length')
RATE=$(echo "scale=2; $FAILED * 100 / $TOTAL" | bc)
# Idempotent create (safe every run)
curl -fsS -X POST https://app.draxis.ai/api/mcp \
-H "Authorization: Bearer $DRAXIS_PAT" \
-H 'content-type: application/json' \
-d "$(jq -n --arg src "$DRAXIS_SOURCE" '{
jsonrpc: "2.0", id: 1, method: "tools/call",
params: { name: "create_external_push_kri", arguments: {
sourceId: $src, name: "Build Failure Rate 7d",
measurementType: "percentage", unit: "%",
thresholdYellow: 5, thresholdRed: 10
}}}')"
# Push the value
curl -fsS -X POST https://app.draxis.ai/api/mcp \
-H "Authorization: Bearer $DRAXIS_PAT" \
-H 'content-type: application/json' \
-d "$(jq -n --arg src "$DRAXIS_SOURCE" --argjson v "$RATE" '{
jsonrpc: "2.0", id: 2, method: "tools/call",
params: { name: "push_kri_value", arguments: {
sourceId: $src, name: "Build Failure Rate 7d", value: $v
}}}')"
Example: Jira JQL ticket counts (scheduled job)
If your data lives in Jira and a structured connector doesn’t cover the JQL you need, this is the canonical pattern: a scheduled job runs the JQL, computes the count, posts to Draxis. Pseudocode for the HTTP request body:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "push_kri_value",
"arguments": {
"sourceId": "{{ $env.DRAXIS_SOURCE_ID }}",
"name": "Open Critical Bugs",
"value": {{ $json.issues.length }}
}
}
}
One scheduled job per KRI is the easiest mental model. The first run calls create_external_push_kri (you can guard that behind an “If first-run” check, or just call it on every run since the tool is idempotent).
What External Push is NOT
- Not a manual-KRI escape hatch. You still need to provision a real
kri_sourcefrom the admin UI before you can push. The PAT is bound to that source. You cannot “just create a KRI” from MCP without an admin first authorizing the source. - Not a substitute for vendor connectors. If a connector exists for your vendor (Okta, CrowdStrike, KnowBe4, …), use it. Connectors are richer, run on schedule, and handle pagination + retries + rate limits for you. External Push is for the long tail of custom metrics that no connector covers.
- Not an AI extraction surface. Pushed values are ground truth, they bypass the AI proposal pipeline entirely. If you want AI review of unstructured evidence, route it through the AI Drop Zone instead.
- Not a read API. A source-scoped push token cannot call
list_kris,list_risks, or any other read tool. If you need to read tenant data from the same scheduler, mint a separate tenant-wide PAT, never repurpose a push token. - Not authorized to mutate other entities. Pushed values only set
kri.value,kri_status, andlast_updatedon KRIs under the bound source. They do not change risk likelihood/impact, control effectiveness, or any other analyst-owned field.
Safety & limits
- One PAT, one source. The
scope_to_source_idon the token is checked against thesourceIdargument of every push tool call. Mismatch → clear error, no write happens. - Source must be enabled. Disabling the source (the Wi-Fi toggle on the integration card) causes push calls to error with “source disabled”, the right escape hatch for an admin who wants to temporarily quarantine a misbehaving scheduler.
- Audit on every call. Each push writes an
MCP_TOOL_CALLEDrow to the tenant audit log with the token id, tool name, arguments, and IP. Every value change additionally lands inkri_value_historyvia a database trigger. - Idempotent create.
create_external_push_kriusesINSERT…ON CONFLICT(id) DO NOTHING. Re-running it with the same(sourceId, name)is a no-op, no fields are silently overwritten. - Transactional push. The KRI update, source-health update, and integration_run audit row are written in a single transaction. A crash between steps rolls back cleanly, you never get a half-recorded push.
Quirks
- Status defaults to green for untuned KRIs. Draxis doesn’t model an “unmeasured” status, instead the dashboard’s
isKriLivecheck excludes KRIs from breach rollups until their source has had a successful run. After the first push, the KRI is “live”, but if you haven’t set thresholds yet the green / red badge isn’t meaningful. Set thresholds in Settings → KRIs so the color reflects reality. - Renaming the KRI breaks the slug. The deterministic KRI id is a function of
(sourceId, slugified-name). If you changenamein yourcreate_external_push_kricall, a new KRI is created instead of renaming the old one. Pick the name once and keep it; rename in the UI if you want a different display label without changing the slug. - No bulk push. One value per call. For batch loads (initial onboarding of a year of history), loop over the values in your script, the server handles concurrent calls fine.
- OAuth tokens can’t be source-scoped.
scope_to_source_idis a PAT-only affordance, the OAuth path (Claude Desktop, ChatGPT Connectors) doesn’t carry the claim. Use a tenant-wide OAuth grant for human interactive sessions, source-scoped PATs for machine-to-machine push.
Troubleshooting
- HTTP 401, the bearer token is wrong, revoked, expired, or its owning user is disabled. Mint a new push token from the integration’s card.
- “PAT scope-to-source mismatch”, this push token is bound to a different source than the
sourceIdargument. Either fix thesourceIdin your script or mint a token scoped to the right source. - “Tool ‘list_kris’ is not available for source-scoped PATs”, working as intended. Source-scoped tokens only work with the two push tools. Use a tenant-wide PAT (or your interactive session) for read access.
- “KRI not found… call create_external_push_kri first”, the
namedoesn’t match an existing KRI under this source. Common cause: typo, or a previous call used a slightly different name (“Build Failure Rate” vs “Build Failure Rate (7d)” slugify to different ids). Call create with the exact name first; it’s idempotent. - KRI shows up but isn’t in dashboard rollups, your source’s
last_run_statusisn’tok. The push tool sets this automatically, but if it’s blank the source predates the migration; one successful push will flip it. - Backfilled timestamp didn’t land in history, confirm your tenant booted at least once after the 2026-05 release (the trigger migration runs at boot). The boot log shows
[migrate] Rewrote trigger kri_value_history_after_updateon the first eligible boot. - Still stuck? Open a support ticket with the push token id (from Settings → API Tokens), the source id, and the KRI name, and we’ll dig in.