Connect your helpdesk
An outcome inlet is a connector that pulls case outcomes out of your system of record —
Zendesk, Intercom, Salesforce or Jira Service Management — into Neens on a schedule.
Each closed ticket becomes one or more business outcomes (resolution,
escalation, csat, resolution_time, …) keyed to the Neens case the agent actually handled.
It’s the pull half of the outcome inlet. If you already run an ETL or a warehouse job, pushing to
POST /outcomes is simpler and gives you full control over the mapping — see
Business outcomes. Use a connector when you’d rather
not build that job.
Outcome inlets bring in results; trace inlets bring in traces. Different data, different providers, same shape of connector — you can run both.
At a glance
| Where | The Data sources tab of the Business KPIs page (connectors are admin-only), the Helpdesk outcome inlets section |
| Providers | Zendesk, Intercom, Salesforce, Jira Service Management |
| Key API | GET/POST /outcome-inlets, PATCH/DELETE /outcome-inlets/{id}, POST /outcome-inlets/{id}/test, POST /outcome-inlets/{id}/sync |
| Direction | Inbound — Neens polls the provider and records new outcomes on a schedule |
| Scope | Agent-scoped: outcomes land in the connector’s agent |
| Egress safety | Every fetch is SSRF-gated; provider clouds work out of the box |
| LLM | None. Provider fields are mapped deterministically; nothing is inferred |
Add a connector
Every route on this tab is admin-only — a connector holds a credential for an external system and its base URL is dialled server-side.
Open the Data sources tab
On the Business KPIs page, open the Data sources tab. Under Helpdesk outcome inlets, click Add inlet and pick the Provider.
Fill in the common fields
| Field | Meaning |
|---|---|
| Name | A label for this connector in the list. |
| Base URL | The provider API origin (see the per-provider setup below). |
| Account email | Zendesk and Jira Service Management only — the account the API token belongs to. |
| Poll interval (minutes) | How often Neens syncs this connector. Default 15. |
| Backfill window (days) | How far back the first sync reaches. Default 7. |
| Enabled | Off by default — create it disabled, Test it, then turn it on. |
Set the correlation contract
This is the part that decides whether your outcomes match anything, and it is worth two minutes of thought. Two settings, shared by all four providers:
| Field | Meaning |
|---|---|
| Correlation type | conversation (default) · session · external — how Neens matches an inbound record to a case. See choosing a correlation type. |
| Correlation field (path on the provider record) | The path on the provider’s record whose value becomes the correlation key. Leave blank for the provider default. |
The two combine like this:
- Your agent uses the ticket id as its conversation id. Correlation type
conversation, correlation field the provider’s own record id (idfor Zendesk/Intercom,Idfor Salesforce,keyfor Jira). This is the default and the cleanest setup. - Your agent writes the ticket id into its trace metadata. Correlation type
external, and the correlation field is still the provider’s ticket id — Neens then looks that value up in your sessions’ metadata. - The provider record stores the Neens conversation id in a custom field. Correlation type
conversation, correlation field that custom field (external_idon Zendesk,custom_attributes.neens_conversationon Intercom, acustomfield_…on Jira, a custom field on Salesforce).
If the correlation field resolves to nothing on a record, that record is skipped and counted —
never guessed into a match. The connector reports partial with the number skipped and the field
name, so a typo is visible on the very first sync instead of silently halving your denominator.
Add the credential
Credentials are write-only: encrypted the moment you save, never returned by any API (responses report only whether one is set). When you edit a connector later, leave the field blank to keep the stored secret; type a value only to replace it.
Test before you enable
Click Test. Neens makes one bounded, SSRF-gated call and reports an honest result — reachable — N recent ticket(s) visible, or the real error (401, a timeout, a blocked host). Fix anything
broken, then enable.
Enable, or sync now
Once enabled the connector syncs on its poll interval. Sync now runs one bounded sync immediately and reports how many outcomes were recorded and how many matched.
Zendesk
| Field | Value |
|---|---|
| Base URL | https://<subdomain>.zendesk.com |
| The Zendesk account the API token belongs to (required). | |
| Credential | A Zendesk API token (Admin Center → Apps and integrations → APIs → Zendesk API). |
| Default correlation field | id (the ticket id) |
Neens authenticates with HTTP Basic using Zendesk’s API-token scheme ({email}/token as the
username) and reads the cursor-based Incremental Ticket Export
(/api/v2/incremental/tickets/cursor), side-loading Ticket Metrics so resolution and reply times
are available.
What maps:
| Outcome | From |
|---|---|
resolution | status — solved or closed is true. Emitted for every status, so an open ticket is an honest false that later restates to true. |
outcome_label | The raw status. |
csat | satisfaction_rating.score — good → 1.0, bad → 0.0, a numeric score passed through. offered/unoffered is omitted: an unanswered survey is not a zero. |
resolution_time | The metric set’s full resolution time (calendar), else solved_at − created_at. |
first_response_time | The metric set’s reply time (calendar). |
reopened | The metric set’s reopen count > 0. |
transfer_count | Assignee stations minus one. |
escalation | An escalation tag you configure, if present in the ticket’s tags; otherwise “the ticket moved between groups”. With neither signal the kind is omitted rather than assumed false. |
Other options: Escalation tag (optional) — the tag your team applies when a ticket goes to a
human. To correlate on a Zendesk custom field, set the correlation field to
custom_fields.<field id>.
Intercom
| Field | Value |
|---|---|
| Base URL | https://api.intercom.io (or the EU/AU regional host your workspace uses) |
| Credential | An Intercom access token. |
| Default correlation field | id (the conversation id) |
Neens authenticates with Authorization: Bearer … plus a pinned Intercom-Version header
(default 2.11, configurable) so a future breaking API version can’t silently reshape the payload,
and reads POST /conversations/search.
What maps:
| Outcome | From |
|---|---|
resolution | state == "closed" (open/snoozed are an honest false). |
outcome_label | The raw state. |
csat | conversation_rating.rating (Intercom’s 1–5 integer). |
first_response_time | statistics.time_to_admin_reply. |
resolution_time | statistics.time_to_last_close, else time_to_first_close. |
reopened | statistics.count_reopens > 0. |
transfer_count | statistics.count_assignments minus one. |
escalation | Only from a configured escalation tag. Intercom has no unambiguous “handed to a human” flag (Fin is itself an admin), so with no tag configured the kind is omitted rather than inferred. |
Other options: API version (optional) and Escalation tag (optional). A stateFilter
(open/closed/snoozed) to sync only closed conversations is available through the API’s
options object. To correlate on a custom attribute, set the correlation field to
custom_attributes.<name>.
Intercom’s search API documents no sort order, so a sync that hits its record budget does not advance the time high-water mark — it hands back Intercom’s own cursor and the next run resumes the identical query mid-scan. Only a scan that completes moves the window forward. Nothing is skipped.
Salesforce
| Field | Value |
|---|---|
| Base URL | Your org’s instance URL — https://<mydomain>.my.salesforce.com |
| Credential | A Salesforce access token. |
| Default correlation field | Id (the Case id) |
Neens authenticates with Authorization: Bearer … and runs a SOQL query over the standard Case
object through the REST Query resource (default API version v60.0).
What maps:
| Outcome | From |
|---|---|
resolution | The standard IsClosed boolean, timestamped at ClosedDate. |
outcome_label | Status. |
escalation | The standard IsEscalated boolean. |
resolution_time | ClosedDate − CreatedDate. |
csat | Only from a CSAT field you configure. Salesforce ships no standard CSAT field on Case, so Neens never invents one. |
Other options: API version (optional), CSAT field (optional) and Extra WHERE clause
(optional) to narrow the export. A different object (if you use a custom case object) and
extraFields to widen the SELECT are available through the API’s options object.
Neens stores whatever token you give it. A short-lived token simply fails the next sync with an
honest 401 — the cursor doesn’t advance and no outcome is fabricated.
Jira Service Management
| Field | Value |
|---|---|
| Base URL | https://<site>.atlassian.net for Cloud, or your Data Center origin |
| The Atlassian account the API token belongs to (required). | |
| Credential | An Atlassian API token. |
| Default correlation field | key (the issue key) |
Neens authenticates with HTTP Basic ({email}:{api token}) and runs a JQL search. Atlassian split
its search endpoints, so Neens drives Jira Cloud’s JQL enhanced search by default and falls back
once to the classic Data Center endpoint when Cloud answers 404/410, then sticks with whichever
works. Pin it explicitly by setting the apiVariant option (cloud or datacenter) through the
API.
What maps:
| Outcome | From |
|---|---|
resolution | fields.resolution is non-null, timestamped at resolutiondate. |
outcome_label | The resolution name when resolved, else the status name. |
resolution_time | resolutiondate − created. |
escalation | Only from an escalationField (a customfield_… path) configured through the API’s options object, optionally compared against an escalationValue. Jira ships no standard escalation flag. |
csat | Only from a configured CSAT field (optional). JSM’s satisfaction rating lives in a site-specific customfield_…, so there is no default to hard-code. |
Other options: Project key (optional) and a JQL filter (optional) to scope the export.
JQL date comparisons are interpreted in the credential user’s Jira time zone, not UTC. The five-minute re-fetch overlap plus content dedup absorbs the skew, but set the connector’s Jira user to UTC if you want the window to be exact.
How syncing works
A scheduler periodically syncs the enabled connectors that are due — whose last run is older
than their poll interval (default 15 minutes, set per connector).
- First sync — backfill. The first sync reaches back backfill days (default
7). - Incremental syncs. After that each sync fetches only records updated since the last successful high-water mark, with a five-minute overlap so nothing on the boundary is missed.
- A failure never advances anything. On any error — a
401, a timeout, a blocked host, a database problem — the cursor and the high-water mark are left exactly where they were, so the next run retries the same window. Nothing is invented to fill the gap. - Re-fetching is free. Records go through the same content-hash dedup as pushed outcomes: an unchanged re-poll is deduplicated, and a genuinely changed value becomes a new revision. A ticket reopening is a restatement you can see, not a duplicate row.
Read the match counters
Every connector row in the list shows its own honesty numbers after each run:
| Column | What it shows |
|---|---|
| Name | The connector name, with its correlation contract underneath — keyed by external · ticket_id. |
| Status | never · ok · partial · error, plus how long ago the last run was, and the real error string inline when a run failed. partial means records were skipped because the correlation field resolved to nothing — the message says how many and which field. |
| Last sync | The match counters: N outcomes · X% matched, and below it M matched, U unresolved. unresolved folds in ambiguous, because “did not resolve to exactly one case” is the number that matters. A non-zero unresolved count is highlighted. |
| Enabled | The on/off toggle for the scheduled sync. |
The same row carries the Test, Sync now, Edit and Delete actions.
A connector whose matched percentage is far below 100% is a correlation-contract problem, not a provider problem. Open the Match coverage panel at the top of the same tab, read the per-correlation-type breakdown and the recent unmatched sample, and work through Why your match rate is not 100%.
Security
- Credentials encrypted at rest. Every provider secret is Fernet-encrypted before storage and is never returned by any API — responses report only whether a credential is set.
- SSRF egress gate. Every outbound fetch and every Test probe resolves and pins the host,
refuses redirects, and caps the response body. A private, loopback, link-local or reserved address
is refused. A literal private IP is also rejected at save time. Provider clouds
(
*.zendesk.com,api.intercom.io,*.my.salesforce.com,*.atlassian.net) are public and reachable out of the box. - Nothing is written back. An inlet only pulls. Neens never modifies a ticket, and no trace content is sent to the provider.
- Admin-only. Creating, editing, testing and syncing a connector all require the admin role, and every change is written to the audit log.
Limits
Each sync is bounded so a single connector can’t overwhelm a worker: a connector records up to 500 outcomes per run (a larger backlog drains across successive runs), each provider response body is capped at 8 MiB, and a single outbound call is allowed 30 seconds. The per-connector poll interval (default 15 minutes) and backfill window (default 7 days) are set on the connector itself when you create it.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Test returns 401 | Wrong or revoked credential, or the wrong email for a Basic-auth provider. | Re-copy the token; for Zendesk and Jira check the email matches the account that owns it. |
| Test says the URL is blocked | A self-hosted instance resolves to a private address. | Ask an operator to allowlist the host — a provider on a private address is gated by default. |
Last status is partial | The correlation field resolved to nothing on some records. | Read the message — it names the field. Fix the field path, or narrow the export to records that carry it. |
| Records arrive but almost nothing matches | The correlation type is wrong for how your agent is instrumented. | See Choose a correlation type and the match-rate troubleshooting table. |
| Enabled but nothing syncs | Not due yet, or the first backfill window predates your closed tickets. | Click Sync now, or raise Backfill days. |
A sync shows error | A transient provider or network failure. | The cursor didn’t advance — the next run retries the same window. Fix the cause shown in the last error. |
| The tab isn’t there | You’re not an admin. | Ask an admin. |
See also Business outcomes for the push API, the outcome catalogue and the coverage panel.