Connect outcome sources
A support agent’s outcomes live in a helpdesk (Connect your helpdesk).
Every other agent’s outcomes live somewhere else: a coding agent’s in GitHub, an ops agent’s in
Jira or Linear, a sales agent’s in HubSpot or Salesforce, and a back-office agent’s
often only in your data warehouse. These connectors pull those facts on a schedule and turn
them into the universal task outcomes — task_succeeded, reworked, human_intervention,
time_to_outcome — that the Business KPIs read.
At a glance
| Where | Business KPIs → Setup → Advanced → Data sources. Connectors are grouped by the kind of agent they serve: Coding (GitHub), Back-office / documents (Snowflake, BigQuery), Ops / SRE (Jira, Linear), Sales / GTM (HubSpot, Salesforce CRM), Support (the four helpdesks), and General agent · Research / analyst (no connector — push to POST /outcomes). Each group has its own Add source button. |
| Examples | End-to-end walkthroughs with numbers: a coding agent on GitHub and a claims agent on Snowflake. |
| Who | Admins add and edit connectors; everyone with read access sees the match coverage. |
| Key API | GET /outcome-inlets/providers (the catalogue and its field mapping), POST /outcome-inlets/preview (map five records without saving), and the same POST/PATCH/DELETE /outcome-inlets, /test and /sync routes as the helpdesk connectors. |
| Credentials | Encrypted at rest and write-only: Neens never shows a saved credential again. GitHub reuses the agent’s existing GitHub App installation, so it stores no credential of its own. |
| LLM | None. Every outcome is read from a field the provider documents; nothing is inferred. |
Before you save: field mapping and a five-record preview
Every connector form shows How provider fields become outcomes: which field on the provider’s record becomes which outcome kind, and the rule applied. Below it, Preview 5 records fetches up to five records with the settings you typed and shows exactly what each one maps to — the record, its correlation key, and every outcome it produces.
Nothing is saved by a preview — not the connector, not the credential, not an outcome. The credential you typed is used for that one fetch only. When you edit a saved connector, the preview uses the stored credential unless you type a new one. A stored credential is only ever sent to the base URL it was saved with: to preview a saved connector against a different base URL, type the credential again.
Add a source with the API
The same steps work over the API with a company admin key (a project nk_live_… key cannot
manage connectors). Preview first, then save the same body:
cat > source.json <<'JSON'
{
"provider": "snowflake",
"name": "Claims outcomes",
"baseUrl": "https://acme-analytics.snowflakecomputing.com",
"credential": "<programmatic access token>",
"options": {
"warehouse": "ANALYTICS_WH",
"role": "ANALYTICS_READER",
"correlationType": "external",
"cursorColumn": "updated_at",
"query": "SELECT claim_id AS correlation_key, 'task_succeeded' AS kind, status = 'APPROVED' AS value, decided_at AS occurred_at, updated_at FROM claims.decisions WHERE updated_at > :since"
},
"enabled": true
}
JSON
# Map up to five records without saving anything
curl -sf -X POST "$NEENS_BASE_URL/api/outcome-inlets/preview" \
-H "Authorization: Bearer $NEENS_API_KEY" -H "Content-Type: application/json" \
-d @source.json
# Save the connector (the response never includes the credential)
curl -sf -X POST "$NEENS_BASE_URL/api/outcome-inlets" \
-H "Authorization: Bearer $NEENS_API_KEY" -H "Content-Type: application/json" \
-d @source.jsonA GitHub source takes no credential and no baseUrl; it names the VCS installation instead:
{
"provider": "github",
"name": "storefront PRs",
"options": {
"vcsInstallationId": "<id from Settings → VCS installations>",
"repo": "acme/storefront",
"agentLogins": "my-agent[bot]",
"correlationType": "external"
},
"enabled": true
}GET /api/outcome-inlets/providers lists every provider’s option names, default base URL and
field mapping.
A connector saved with Enabled unchecked does not sync on its schedule. The form says so, and the list shows a notice with an Enable now button. Sync now still runs a one-off pull. Each row in the list shows its Status (OK, Partial, Error or Never run), its Match counts (for example 1,240 outcomes · 97.6% matched) and an Enabled switch, with Test, Sync now, Edit and Delete.
What gets skipped, and why
A connector never guesses. Three things are counted and shown on the connector row instead:
| What | Why it is skipped | What you see |
|---|---|---|
| A record whose correlation field resolves to nothing | A guessed key produces a confidently wrong match | Partial status, with the count and the field name |
A warehouse row with an unknown kind, a null value or an unreadable occurred_at | It cannot be recorded as an outcome as-is | Partial status, with a count per reason |
| A revert that cannot be traced back to a pull request | Attributing it to the wrong PR would invent rework | Partial status, with the count |
A signal the provider does not let Neens see is left out rather than reported as “no”: human intervention is only reported when the agent’s identity is known (see each provider below), and a truncated history never becomes “no human stepped in”. Outcomes that resolve to no task yet are kept and retried, as with every outcome — see Business outcomes.
GitHub — coding agents
A coding agent’s unit of work is a pull request. The GitHub connector reads the repository’s pull requests with the GitHub App installation you already set up under Settings → VCS installations, so there is no second credential.
| Provider field | Outcome | Rule |
|---|---|---|
merged_at | task_succeeded = true | The PR merged |
closed_at (not merged) | task_succeeded = false | Closed without merging |
merged_at − created_at | time_to_outcome | Cycle time of a merged PR |
| A revert of the PR | reworked = true | Stamped at the time of the revert, so the KPI’s rework window is measured from the merge |
commits[].author.login | human_intervention | true when anyone but the agent pushed a commit |
A revert is found two ways: a pull request created with GitHub’s Revert button (its description names the reverted PR), and a commit on the base branch whose message carries git’s standard “This reverts commit …” line. An open PR has no outcome yet; the KPI’s no-outcome timeout decides when it counts as abandoned.
Check the GitHub App’s permissions
The installation needs Pull requests: read and Contents: read on the repository.
Add the source
In Data sources, under Coding, click Add source. Pick the GitHub App installation,
and set the Repository (owner/repo) if it differs from the installation’s. The connector
calls the same GitHub API host as the installation — for GitHub Enterprise, set the API host on the
VCS installation, not on the connector.
Tell Neens who the agent is
Set Agent GitHub logins to the account your agent opens PRs with — usually its App’s bot
account, for example my-agent[bot]. Only PRs those logins opened count as the agent’s tasks, and
a commit by anyone else counts as a human stepping in. Left blank, each PR’s author is taken to be
the agent.
Key PRs to tasks
By default the correlation field is the PR’s html_url. Have your agent emit that URL as its task
id — the neens.task.id span attribute — and set Correlation type to External:
span.set_attribute("neens.task.id", "https://github.com/acme/storefront/pull/4182")If your agent names its task differently, put a line like Neens-Task-Id: TASK-4182 in the PR
description and set the correlation field to trailer:Neens-Task-Id.
Jira and Linear — ops and internal-tooling agents
For agents whose unit of work is an issue: closed, reopened, or handed to a person.
| Provider field | Outcome | Rule |
|---|---|---|
status.statusCategory = done, or a resolution | task_succeeded | true, unless the resolution is Won’t Do, Won’t Fix, Duplicate, Cannot Reproduce or Declined |
resolutiondate − created | time_to_outcome | For a successful issue |
| Changelog: status moved out of Done, Closed or Resolved, or the resolution cleared | reworked = true | At the time of the reopen |
| Changelog: assignee | human_intervention | true when the issue was assigned to someone outside Agent account IDs |
Connect with the site URL (https://your-site.atlassian.net), the account email and an
Atlassian API token. Narrow the sync with a Project key or a JQL filter. Set Agent
account IDs to the Jira account your agent works as; without it, human intervention is not
reported. Jira returns only the most recent part of a long issue history; when the history is cut
short, Neens does not report “no human stepped in” for that issue.
The correlation field defaults to the issue key (for example OPS-412). Emit it as
neens.task.id, and use Correlation type External.
HubSpot and Salesforce — sales and GTM agents
For agents that book meetings, create opportunities or move deals forward.
Connect with a private app token (it needs read access to the objects you sync); the base URL
defaults to https://api.hubapi.com. Pick the Object:
- Meetings (meeting booked). The meeting outcome decides the verdict: by default scheduled, completed and rescheduled count as a success, no show and canceled as a failure. The outcome is stamped when the meeting was booked.
- Deals (stage reached). List the deal stage IDs that count as a success (for example
closedwon) and as a failure (closedlost). Stage IDs are specific to your HubSpot account, so nothing is counted until you list them. A successful deal also records its time to outcome.
Store the agent’s task id on the record in a custom property (for example neens_task_id) and set
it as the correlation field.
A warehouse query — any agent, any system
When the outcome lives in a back-office system with no API — a claims platform, an ERP, an internal tool — but lands in your warehouse, write one query that returns one row per outcome. Neens runs it on the connector’s schedule against Snowflake or BigQuery.
| Column | Required | Meaning |
|---|---|---|
correlation_key | yes | The task’s key — the value your agent emits as neens.task.id |
kind | yes | An outcome kind, for example task_succeeded, human_intervention, reworked, accepted |
value | yes | true/false for a boolean kind; a number of milliseconds for time_to_outcome |
occurred_at | yes | When the outcome happened |
updated_at | only if the cursor column is updated_at | When the row last changed |
Filter on :since. Neens binds the time of the last sync there as a query parameter — it is never
pasted into your SQL — and only fetches rows newer than it, oldest first, a bounded number per sync.
Choose updated_at as the Cursor column when rows are corrected in place, so a correction is
picked up again.
A claims-processing agent, recording whether each claim went straight through, whether a person had to touch it, and whether it was reprocessed:
SELECT c.claim_id AS correlation_key,
'task_succeeded' AS kind,
c.status = 'APPROVED' AS value,
c.decided_at AS occurred_at,
c.updated_at AS updated_at
FROM claims.decisions c
WHERE c.decided_by_agent
AND c.updated_at > :since
UNION ALL
SELECT c.claim_id, 'human_intervention', c.manual_review_count > 0, c.decided_at, c.updated_at
FROM claims.decisions c
WHERE c.decided_by_agent
AND c.updated_at > :since
UNION ALL
SELECT r.claim_id, 'reworked', TRUE, r.reprocessed_at, r.reprocessed_at
FROM claims.reprocessing r
WHERE r.reprocessed_at > :sinceThe query must be one read-only SELECT (or WITH … SELECT). Anything else — a second
statement, INSERT, UPDATE, DELETE, MERGE, CREATE, CALL, COPY, SET and the like —
is refused when you save, not just when it runs. Write string literals with single quotes:
Snowflake $$…$$ strings and BigQuery raw strings that contain a backslash are refused too.
Connect with a role that can only read the tables the query names.
The base URL is your account URL (https://<orgname>-<accountname>.snowflakecomputing.com). Pick
the Authentication:
- Programmatic access token — paste the token.
- Key pair (JWT) — set the User and paste the PEM private key whose public key is assigned to that user.
Set the Warehouse, and the Role, Database and Schema if they differ from the user’s defaults. The query runs with a time limit, and only one statement is accepted.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Partial: records skipped, correlation field resolved to no value | The records don’t carry the field you named | Check the field in Preview 5 records; for GitHub, check that the agent sets the PR URL or the trailer: line |
| Outcomes arrive but stay unmatched | The key the source sends is not the key your agent emits | Compare a previewed correlation key with the agent’s neens.task.id; correlation type must be External for an agent-emitted id |
| GitHub: “the GitHub source’s VCS installation no longer exists” | The installation was deleted | Pick another installation, or add one under Settings → VCS installations |
| GitHub: no human intervention reported | Some commits are not linked to a GitHub account, or the PR has 250+ commits | Expected — Neens can’t tell who pushed them, so it reports nothing rather than “no” |
| Jira: no human intervention reported | Agent account IDs is empty | Add the account your agent works as |
| HubSpot deals: nothing recorded | No success or failure stages listed | List the stage IDs under Counts as success and Counts as failure |
| Warehouse: “The query must filter on the incremental cursor with :since” | The query has no :since | Filter on it, for example WHERE updated_at > :since |
| Warehouse: “the query must return the columns …” | A required column is missing or misnamed | Alias the columns exactly: correlation_key, kind, value, occurred_at |