GuidesConnectorsConnect outcome sources

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

WhereBusiness 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.
ExamplesEnd-to-end walkthroughs with numbers: a coding agent on GitHub and a claims agent on Snowflake.
WhoAdmins add and edit connectors; everyone with read access sees the match coverage.
Key APIGET /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.
CredentialsEncrypted 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.
LLMNone. 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.json

A 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:

WhatWhy it is skippedWhat you see
A record whose correlation field resolves to nothingA guessed key produces a confidently wrong matchPartial status, with the count and the field name
A warehouse row with an unknown kind, a null value or an unreadable occurred_atIt cannot be recorded as an outcome as-isPartial status, with a count per reason
A revert that cannot be traced back to a pull requestAttributing it to the wrong PR would invent reworkPartial 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 fieldOutcomeRule
merged_attask_succeeded = trueThe PR merged
closed_at (not merged)task_succeeded = falseClosed without merging
merged_at − created_attime_to_outcomeCycle time of a merged PR
A revert of the PRreworked = trueStamped at the time of the revert, so the KPI’s rework window is measured from the merge
commits[].author.loginhuman_interventiontrue 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 fieldOutcomeRule
status.statusCategory = done, or a resolutiontask_succeededtrue, unless the resolution is Won’t Do, Won’t Fix, Duplicate, Cannot Reproduce or Declined
resolutiondate − createdtime_to_outcomeFor a successful issue
Changelog: status moved out of Done, Closed or Resolved, or the resolution clearedreworked = trueAt the time of the reopen
Changelog: assigneehuman_interventiontrue 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.

ColumnRequiredMeaning
correlation_keyyesThe task’s key — the value your agent emits as neens.task.id
kindyesAn outcome kind, for example task_succeeded, human_intervention, reworked, accepted
valueyestrue/false for a boolean kind; a number of milliseconds for time_to_outcome
occurred_atyesWhen the outcome happened
updated_atonly if the cursor column is updated_atWhen 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 > :since
⚠️

The 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

SymptomCauseFix
Partial: records skipped, correlation field resolved to no valueThe records don’t carry the field you namedCheck the field in Preview 5 records; for GitHub, check that the agent sets the PR URL or the trailer: line
Outcomes arrive but stay unmatchedThe key the source sends is not the key your agent emitsCompare 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 deletedPick another installation, or add one under Settings → VCS installations
GitHub: no human intervention reportedSome commits are not linked to a GitHub account, or the PR has 250+ commitsExpected — Neens can’t tell who pushed them, so it reports nothing rather than “no”
Jira: no human intervention reportedAgent account IDs is emptyAdd the account your agent works as
HubSpot deals: nothing recordedNo success or failure stages listedList 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 :sinceFilter on it, for example WHERE updated_at > :since
Warehouse: “the query must return the columns …”A required column is missing or misnamedAlias the columns exactly: correlation_key, kind, value, occurred_at