PerfCopilot
HomeDocs
Documentation

Everything you need to write a fair review.

From your first connection to a sealed, delivered review. Set up PerfCopilot, connect your tools, and generate your first cited draft.

Jira

Jira is a dual-purpose source in PerfCopilot. It contributes both signals (tickets you closed, blockers you flagged, work you have open) and goals (issues explicitly tracked as objectives). Both flow into reviews — signals as quantitative evidence, goals as a separate tab in the dashboard.

What we pull

Signals (source = "jira")

For each employee in the active cycle's window, the ingester runs three JQL queries against your Jira instance:

  • Tickets closedassignee = "<jira_id>" AND statusCategory = Done AND resolved >= start AND resolved <= end. The headline metric for cohort baselines (tickets_closed).
  • Blocker flags — issues the assignee flagged as blockers in the cycle window.
  • Open work — currently open tickets assigned to them, regardless of created date. A point-in-time snapshot of what's on their plate.

Each ticket lands as a RawSignal row with the ticket key, summary, type, status, and resolved-at timestamp in the payload. The dashboard's signals strip groups them by week within the cycle.

Goals (source = "goals")

Separate flow. Jira issues that you've configured as goals are pulled into the goals table — distinct from raw_signals. They show up under the Goals tab on each employee's dashboard.

The goal sync uses each employee's Jira account to find issues where:

  • assignee = "<their jira_id>"
  • issuetype in (<your configured Goal issue types>)

The exact JQL is assignee = "<id>" AND issuetype in ("Goal", "Epic", ...) — see Goal sync configuration below for the two fields you set.

Each matching issue is upserted as a Goal row. The status maps as:

| Jira | PerfCopilot | |---|---| | Status category = Done | completed | | Label includes off-track / off_track | off_track | | Label includes at-risk / at_risk / blocked | at_risk | | Otherwise | on_track |

Period: period_start is the issue's created timestamp; period_end is duedate (anchored to 23:59:59 UTC) or created + 90 days as a fallback. Goals whose period falls outside the cycle still show under the All goals filter — only the Active cycle filter intersects with the current cycle's window.

Connecting Jira

Two install patterns:

Cloud (recommended for Atlassian Cloud)

  1. Go to /admin?tab=integrations. Find the Jira card under "Org-wide".
  2. Click Connect. You'll be redirected to Atlassian's OAuth consent screen.
  3. Grant access to the Jira site you want to sync from.
  4. Fill in the Goal sync configuration below if you want to use the Goals tab.
  5. Match Jira accounts to PerfCopilot employees. The Jira card shows an unmapped-employees list — for each, paste their Jira accountId (visible in the URL when you visit their Jira profile, or via the Jira admin user list). Without a mapped accountId the ingester has nothing to query against.

Server / Data Center

API-token-based install. In the same card, switch to Server / Data Center mode and provide:

  • Your Jira base URL (https://jira.your-company.com)
  • An API token from a service account that has read access to the projects you want to pull from
  • The same Goal sync configuration applies.

Same accountId mapping applies.

Goal sync configuration

Two fields on the Jira integration card govern what the Sync from Jira button on the Goals tab does. Both edit the integration's extra_config.

Goal issue types (comma-separated)

Required. A comma-separated list of Jira issue type names that should be pulled as goals. Examples:

  • Goal
  • Goal, OKR
  • Epic, Initiative
  • Goal, Objective

Names are case-sensitive and must match the issue type names as they appear in your Jira instance — including any custom types your team has defined. Whitespace around the commas is tolerated.

The ingester runs this JQL per employee:

assignee = "<their jira_id>" AND issuetype in (<your list, quoted>)

If this field is empty, the sync errors out with JiraGoalsConfigError and nothing is pulled. Set it before clicking Sync from Jira for the first time.

Child types treated as Key Results (optional)

Optional. A comma-separated list of issue type names whose instances — when they're children of a synced goal — should be treated as Key Results under that goal.

How it works:

  • After each goal is upserted, the ingester walks one level of children using parent = "<goal_key>" AND issuetype in (<child types>).
  • Each matching child becomes a GoalKeyResult row attached to the parent goal — surfaced in the dashboard's Goals tab as nested progress markers under the parent.
  • Goal status (on_track, at_risk, off_track, completed) on the parent is unaffected by the children; KRs carry their own status derived the same way.

Examples for common setups:

  • OKR-style with Initiatives as goals and Stories as the measurable work: enter Story (or Story, Task).
  • Lean OKR with Goals containing Tasks: enter Task.
  • Quarterly objectives with sub-objectives: enter the sub-objective issue type name.

Leave empty if you don't want PerfCopilot to walk children — goals will still sync, just without nested KRs. The walk is capped at 50 children per goal so an over-broad config can't run away.

Heads up: if you change either field after syncing, the next sync re-evaluates the JQL and any goals that no longer match are not deleted — they stay in the dashboard until you delete them manually. Same for KRs whose parent issue type was removed from the list.

What gets into a review

For each generated review, the AI prompt sees a [JIRA DATA] block summarizing the cycle's signals:

[JIRA DATA]
tickets_closed:    14
blocker_flags:      2
open_assigned:     11
top_projects:      [{"key": "PERF", "count": 9}, {"key": "WEB", "count": 5}]

Plus a [BASELINES] row with the team / department / org cohort median for tickets_closed, so claims like "above the team median" stay grounded.

Goals are surfaced as a separate [GOALS] block listing each goal's name, status (on-track / at-risk / off-track / done), and key results.

Troubleshooting

"Signals strip shows zero Jira tickets"

Most common causes, in order:

  1. No accountId mapping. Check the Jira card on /admin?tab=integrations — unmapped employees can't be queried.
  2. Wrong cycle window. Jira's resolved field is the close timestamp. If a manager sets a custom cycle that spans, say, a holiday week with no resolves, the ingester correctly returns zero. Try a wider window or All goals to verify data exists.
  3. JQL permission boundary. The OAuth scope (or API token's user) needs read access to the projects the assignee works in. If a project is restricted, those issues silently don't return. The Jira admin can help confirm.
  4. The assignee field is unset on closed issues. Some teams close tickets without an assignee (rare but happens with automation). Those won't pull. Reassign before close, or accept the gap.

"Goals sync runs but pulls zero goals"

  1. Goal issue types is empty in the Jira card. The sync errors out with JiraGoalsConfigError — set the field. See Goal sync configuration.
  2. Issue type name mismatch. The names in Goal issue types are case-sensitive and must match Jira exactly. goal won't match Goal. Check the spelling on a Jira issue under Issue Type.
  3. Issues exist but aren't assigned. Goals are scoped to the assignee. Self-managed goals where assignee is unset won't pull.
  4. Sync hit the 200-issue cap. The ingester returns at most 200 goals per sync (per employee) and flags truncated: true in the result. If you have more than 200 active goals per person, narrow Goal issue types to the most-specific type you actually use.

"Goals sync but no Key Results show up"

  1. Child types treated as Key Results is empty. Goals will still sync, just flat. Add the child issue type name to enable the walk.
  2. Children aren't parent-linked to the goal. The walk uses parent = "<goal_key>" — if your team uses a custom "Parent Link" field instead of the standard parent relation, the walk won't find them.
  3. Children exceed 50 per goal. The KR walk caps at 50 per parent. Excess children are silently dropped.

"Sync says it succeeded but nothing changed"

The ingester's dedup key is (employee_id, source="jira", cycle_id, ticket_key). Re-running on the same cycle is idempotent — same data → no new rows. If you expected new tickets, check that they were actually closed in the cycle window, not just before it.

Privacy notes

  • We pull issue metadata only — key, summary, status, type, assignee, labels, resolution date. We don't pull comments, attachments, or descriptions.
  • Issue summaries can contain customer or sensitive data; they're stored in raw_signals.payload and visible to managers viewing the dashboard. If your team has a sensitivity classification on tickets, configure JQL filters to exclude them.
  • The Jira API is rate-limited per Atlassian's published quotas. PerfCopilot batches requests within those limits; on hitting a 429, the ingester waits and retries.