GuidesConnectorsConnect your helpdesk

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

WhereThe Data sources tab of the Business KPIs page (connectors are admin-only), the Helpdesk outcome inlets section
ProvidersZendesk, Intercom, Salesforce, Jira Service Management
Key APIGET/POST /outcome-inlets, PATCH/DELETE /outcome-inlets/{id}, POST /outcome-inlets/{id}/test, POST /outcome-inlets/{id}/sync
DirectionInbound — Neens polls the provider and records new outcomes on a schedule
ScopeAgent-scoped: outcomes land in the connector’s agent
Egress safetyEvery fetch is SSRF-gated; provider clouds work out of the box
LLMNone. 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

FieldMeaning
NameA label for this connector in the list.
Base URLThe provider API origin (see the per-provider setup below).
Account emailZendesk 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.
EnabledOff 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:

FieldMeaning
Correlation typeconversation (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 (id for Zendesk/Intercom, Id for Salesforce, key for 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_id on Zendesk, custom_attributes.neens_conversation on Intercom, a customfield_… 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

FieldValue
Base URLhttps://<subdomain>.zendesk.com
EmailThe Zendesk account the API token belongs to (required).
CredentialA Zendesk API token (Admin Center → Apps and integrations → APIs → Zendesk API).
Default correlation fieldid (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:

OutcomeFrom
resolutionstatus — solved or closed is true. Emitted for every status, so an open ticket is an honest false that later restates to true.
outcome_labelThe raw status.
csatsatisfaction_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_timeThe metric set’s full resolution time (calendar), else solved_at − created_at.
first_response_timeThe metric set’s reply time (calendar).
reopenedThe metric set’s reopen count > 0.
transfer_countAssignee stations minus one.
escalationAn 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

FieldValue
Base URLhttps://api.intercom.io (or the EU/AU regional host your workspace uses)
CredentialAn Intercom access token.
Default correlation fieldid (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:

OutcomeFrom
resolutionstate == "closed" (open/snoozed are an honest false).
outcome_labelThe raw state.
csatconversation_rating.rating (Intercom’s 1–5 integer).
first_response_timestatistics.time_to_admin_reply.
resolution_timestatistics.time_to_last_close, else time_to_first_close.
reopenedstatistics.count_reopens > 0.
transfer_countstatistics.count_assignments minus one.
escalationOnly 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

FieldValue
Base URLYour org’s instance URL — https://<mydomain>.my.salesforce.com
CredentialA Salesforce access token.
Default correlation fieldId (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:

OutcomeFrom
resolutionThe standard IsClosed boolean, timestamped at ClosedDate.
outcome_labelStatus.
escalationThe standard IsEscalated boolean.
resolution_timeClosedDate − CreatedDate.
csatOnly 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

FieldValue
Base URLhttps://<site>.atlassian.net for Cloud, or your Data Center origin
EmailThe Atlassian account the API token belongs to (required).
CredentialAn Atlassian API token.
Default correlation fieldkey (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:

OutcomeFrom
resolutionfields.resolution is non-null, timestamped at resolutiondate.
outcome_labelThe resolution name when resolved, else the status name.
resolution_timeresolutiondate − created.
escalationOnly from an escalationField (a customfield_… path) configured through the API’s options object, optionally compared against an escalationValue. Jira ships no standard escalation flag.
csatOnly 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:

ColumnWhat it shows
NameThe connector name, with its correlation contract underneath — keyed by external · ticket_id.
Statusnever · 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 syncThe 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.
EnabledThe 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

SymptomLikely causeFix
Test returns 401Wrong 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 blockedA 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 partialThe 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 matchesThe correlation type is wrong for how your agent is instrumented.See Choose a correlation type and the match-rate troubleshooting table.
Enabled but nothing syncsNot due yet, or the first backfill window predates your closed tickets.Click Sync now, or raise Backfill days.
A sync shows errorA 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 thereYou’re not an admin.Ask an admin.

See also Business outcomes for the push API, the outcome catalogue and the coverage panel.