Skip to main content

Uploading runs

hajer eval --upload sends each suite's payload to Hajer once the engine has finished. The platform attaches the run to the linked GitHub repository named by the run's git context, and it appears on that repository's Tests page.

Before you upload​

  1. Create a team API key in Settings → API keys. Set HAJER_API_KEY and HAJER_TEAM_ID from the values shown.
  2. Connect the Hajer GitHub App in Settings → GitHub and link the repository with Connect repository.
export HAJER_API_KEY=...
export HAJER_TEAM_ID=...
# export HAJER_BASE_URL=https://api.hajer.ai # the default; change only for a self-hosted platform
hajer eval --upload

Without HAJER_API_KEY and HAJER_TEAM_ID, or with HAJER_DISABLED=1, nothing is sent and the upload is reported skipped (INERT). A pull request from a fork, whose CI has no secrets, still runs its evals and passes or fails on their results.

The request​

POST {HAJER_BASE_URL}/api/teams/{HAJER_TEAM_ID}/eval-runs
Authorization: Bearer <HAJER_API_KEY>
Content-Type: application/json
Idempotency-Key: <runId>

The body is the payload as compact JSON with sorted keys. The run id is the idempotency key on every attempt, so retrying the same upload stores the run once.

Retries​

ResponseWhat hajer eval does
2xxuploaded.
409 ConflictTreated as uploaded: a run with this id is already stored.
any other 4xxfailed (REFUSED). Not retried; the same body would get the same answer.
5xx, connection errorRetried. After the last attempt, failed (UNREACHABLE).
timeoutRetried. After the last attempt, failed (TIMEOUT).

There are 3 attempts by default (HAJER_EVAL_UPLOAD_ATTEMPTS). The wait between attempts starts at 200 ms and doubles, capped at 30 s (HAJER_EVAL_UPLOAD_BACKOFF_INITIAL_MS, HAJER_EVAL_UPLOAD_BACKOFF_MAX_MS). Each request has a 30 s deadline (HAJER_EVAL_UPLOAD_DEADLINE_MS).

On the platform side, re-sending the identical payload answers 200, and a first upload answers 201. Sending a different payload under a run id that is already stored answers 409, which hajer eval reports as uploaded; the platform keeps the first one.

Outcomes​

The summary line ends with upload <outcome>:

OutcomeMeaning
not requested--upload was not given, or --no-upload was.
uploadedStored (or already stored).
skipped (INERT)No credentials, or HAJER_DISABLED=1. Nothing was sent.
failed (BODY_OVER_BOUND)Still over the size limit after trimming. Nothing was sent.
failed (REFUSED)The platform answered with a 4xx other than 409. Check the key, the team id and the base URL.
failed (UNREACHABLE)No usable answer after every attempt.
failed (TIMEOUT)The last attempt ran out of time.
failed (NO_RUN_ID)Internal error: the payload had no run id.

Whatever the outcome, the exit code is the engine's, and the payload stays at $HAJER_CACHE_DIR/runs/<run id>/payload.json.

Size limit and trimming​

The body is limited to 8,388,608 bytes (8 MiB, HAJER_EVAL_UPLOAD_MAX_BYTES). If the encoded payload is larger, hajer eval trims it in this order, re-checking after each step:

  1. Remove spans from every result (the spanSummary is kept).
  2. Set output to null on every result.
  3. If it is still too large, send nothing: failed (BODY_OVER_BOUND).

The platform enforces the same 8 MiB limit, so raising HAJER_EVAL_UPLOAD_MAX_BYTES past it turns a local refusal into a 413 from the platform (failed (REFUSED)). To make large runs fit, lower HAJER_EVAL_SPANS_MAX or HAJER_EVAL_OUTPUT_MAX_CHARS instead.

Where the run lands​

The payload names no project or repository explicitly. The platform matches it against the team's linked repositories using, in order:

  1. git.ci.repository, when it is an owner/name (GitHub Actions sets this from GITHUB_REPOSITORY),
  2. git.remoteUrl, when it is a github.com remote (HTTPS, SSH or [email protected]:owner/name.git).

Matching is case-insensitive. A run that matches no linked repository is still stored, as an unlinked run of the team. The platform also records whether the run's branch is the repository's default branch.

Git and CI context​

Before the engine starts, hajer eval reads the commit context from git in the suite's directory and from a fixed list of CI variables. Nothing here can fail the run: any value it cannot read is null. Each git call is limited to 5 s (HAJER_EVAL_GIT_TIMEOUT_S).

From git:

  • commitSha: git rev-parse HEAD
  • branch: git rev-parse --abbrev-ref HEAD (null on a detached head)
  • dirty: whether git status --porcelain --untracked-files=no reports changes
  • remoteUrl: remote.origin.url, with credentials removed

From CI, the provider is detected by its own flag, in this order:

Provider (git.ci.provider)Detected byVariables read
github-actionsGITHUB_ACTIONSGITHUB_SHA, GITHUB_REF, GITHUB_REF_NAME, GITHUB_HEAD_REF, GITHUB_BASE_REF, GITHUB_RUN_ID, GITHUB_REPOSITORY, GITHUB_SERVER_URL
gitlab-ciGITLAB_CICI_COMMIT_SHA, CI_COMMIT_REF_NAME, CI_MERGE_REQUEST_IID, CI_MERGE_REQUEST_SOURCE_BRANCH_NAME, CI_MERGE_REQUEST_TARGET_BRANCH_NAME, CI_PIPELINE_ID, CI_PROJECT_URL
circleciCIRCLECICIRCLE_SHA1, CIRCLE_BRANCH, CIRCLE_PULL_REQUEST, CIRCLE_BUILD_NUM, CIRCLE_REPOSITORY_URL
buildkiteBUILDKITEBUILDKITE_COMMIT, BUILDKITE_BRANCH, BUILDKITE_PULL_REQUEST, BUILDKITE_PULL_REQUEST_BASE_BRANCH, BUILDKITE_BUILD_ID, BUILDKITE_REPO
genericCInone

These variables (plus CI) are the only environment variables read for the context; your CI secrets are never read. When a CI provider supplies a commit and branch, they take precedence over the local git values, because CI often checks out a detached merge commit. For a GitHub pull request, the PR number comes from GITHUB_REF (refs/pull/<n>/merge) and the branch from GITHUB_HEAD_REF.

Credential stripping​

Every repository URL, local or from CI, has its credentials removed before it is stored:

InputStored as
https://x-access-token:[email protected]/acme/app.githttps://github.com/acme/app.git
https://[email protected]/acme/app.githttps://github.com/acme/app.git
ssh://git:[email protected]/acme/app.gitssh://[email protected]/acme/app.git
[email protected]:acme/app.gitunchanged

The payload​

One JSON document per suite run, schemaVersion: 1. It is written to payload.json whether or not you upload, and is the exact upload body. Keys are camelCase.

Run fields​

FieldDescription
schemaVersionAlways 1.
runIdevalrun_ plus 32 hex characters, created before the engine starts.
createdAtISO 8601 UTC timestamp of the run's start.
statuspassed, failed, errored, or aborted (no results and an engine exit code other than 0 or 100). Otherwise the worst result outcome.
engineExitCodepromptfoo's exit code.
enginename (promptfoo), version, lockfileDigest, nodeVersion, evalId.
sdkversion of the hajer package.
gitcommitSha, branch, dirty, remoteUrl, and ci with provider, runId, prNumber, baseRef, headRef, repository. null outside a git checkout and outside CI.
suiteIdThe suite's id in hajer.yaml; null for a -c run.
configpath (relative to hajer.yaml for a declared suite), description, providerIds.
filtersworkflowId and obligationIds from --workflow and --obligation.
statstotal, passed, failed, errored, durationMs, tokenUsage, cost.
warningsMetadata warnings such as W_NO_TEST_CASE_ID.
resultsOne entry per test and prompt.

Result fields​

FieldDescription
testCaseIdThe test's id: from its trace, else metadata.testCaseId, else <testIdx>-<promptIdx>.
testIdx, promptIdxpromptfoo's indices.
correlationplatform when the test has a workflowId, else none.
workflowId, obligationIds, componentIds, sourceTraceIds, generatedBy, provenanceFrom the test's effective metadata.hajer.
descriptionThe test's description.
providerid and label.
outcomepassed; errored when the provider produced no output to grade; otherwise failed.
score, errorpromptfoo's score and error message.
assertionsEach with type, metric, passed, score, reason, weight.
latencyMs, tokenUsage, costAs promptfoo reported them.
traceId, evaluationIdpromptfoo's trace and evaluation ids.
outputThe provider's output, redacted, then clipped to HAJER_EVAL_OUTPUT_MAX_CHARS.
spansUp to HAJER_EVAL_SPANS_MAX spans of the test's trace, earliest first: spanId, parentSpanId, name, startTime, endTime (epoch milliseconds), status, attributes.
spanSummaryOver all spans, before the cap: count, errorCount, toolNames, componentIds, workflowIds.

Span attributes are limited to these prefixes: hajer., gen_ai., deployment., tool., session., user., server., error., code.. Everything else a span carries (http.*, db.*, framework attributes) stays on your machine.

Outputs are redacted with the same client-side catalog the tracing SDK uses before they are clipped, so a clip never exposes part of a value the redaction would have caught. HAJER_REDACT_CLIENT=0 turns this off. See Redaction.

Settings​

VariableDefaultPurpose
HAJER_API_KEYnoneTeam API key. Required to upload.
HAJER_TEAM_IDnoneTeam id. Required to upload.
HAJER_BASE_URLhttps://api.hajer.aiThe platform. Change only for a self-hosted platform.
HAJER_DISABLED01 skips the upload.
HAJER_CACHE_DIR$XDG_CACHE_HOME/hajer, else ~/.cache/hajerEngine installs and run directories.
HAJER_EVAL_RUNS_KEEP20Run directories kept in the cache.
HAJER_EVAL_UPLOAD_ATTEMPTS3Attempts per upload.
HAJER_EVAL_UPLOAD_BACKOFF_INITIAL_MS200First wait between attempts.
HAJER_EVAL_UPLOAD_BACKOFF_MAX_MS30000Ceiling of the doubling wait.
HAJER_EVAL_UPLOAD_DEADLINE_MS30000Deadline of one upload request.
HAJER_EVAL_UPLOAD_MAX_BYTES8388608Payload size limit before trimming. The platform also enforces 8 MiB.
HAJER_EVAL_OUTPUT_MAX_CHARS4096Characters of each output kept.
HAJER_EVAL_SPANS_MAX256Spans per result kept; the summary counts all.
HAJER_EVAL_GIT_TIMEOUT_S5Limit on each git read.
HAJER_EVAL_INSTALL_TIMEOUT_S600Limit on the engine install. See The engine.
HAJER_EVAL_OTLP_PORT4318Local trace receiver port; a busy port is replaced by a free one.
HAJER_TRACE_FLUSH_TIMEOUT_MS2000How long a provider waits for its spans to reach the receiver.

Numeric settings must be positive integers. HAJER_EVAL_RUN_ID, HAJER_EVAL_WORKFLOW, HAJER_EVAL_OBLIGATIONS, HAJER_EVAL_HOOK_REPORT, HAJER_EVAL_MANIFEST and HAJER_EVAL_MANIFEST_OBLIGATIONS are set by hajer eval for the engine process; do not set them yourself. For the rest of the SDK's settings, see Configuration.