Privacy policy: Sojourn: Time in Status & Cycle Time for Jira

Vendor: Lichen. Contact: support@lichenapps.com Last updated: 2026-09-11


1. The short version

Sojourn reads the status-change history of the Jira issues it can see and turns it into per-status durations. Everything it derives is stored inside Atlassian's own infrastructure, in storage provisioned for your installation of the app on your site. The app makes no outbound network requests to anywhere except your own Jira site. It has no servers of its own, no analytics, no error-reporting service, and no third-party components that phone home. The vendor cannot read your data.

This is not a policy commitment that could quietly lapse. It is a structural property of how the app is built. A Forge app can only reach the outside world through declared egress or a web trigger, and this app declares neither. That is also what earns it Atlassian's Runs on Atlassian badge: Atlassian evaluates those properties for every version of the app that is deployed, so the claim is checked by the platform and not only asserted here.


2. What the app reads from Jira

The app authenticates as itself (as the app, not as you) and requests exactly these things. One consequence of that is worth stating before the table: the app sees the projects it has been granted rather than the projects you personally can open, so its STORE holds all of them. What you are SHOWN is a different question and a narrower one: every report is restricted to the projects you can browse in Jira, checked as you on every load, so two people looking at the same scope see different numbers. Section 3 says what follows for settings.

WhatWhy
The list of projects it can see, and each project's avatar image addressA whole-instance query is rejected by Jira, so work is partitioned by project. The avatar address arrives in the same response and is used to draw the project's icon beside its key in the report, so two projects with similar keys can be told apart at a glance. The address is never stored, and the picture is loaded by your browser from Atlassian's own API, not by the app and not from anywhere else
Per issue: key, created date, project, status, issue type, assigneeThese six fields and no others. Every search that reads an issue names them explicitly. The one search that builds a JQL scope asks for the issue key alone, which is less, never more
Per issue: the change history (changelog)This is the raw material. It is where "moved from To Do to In Progress at 09:14" lives
A notification when an issue is updatedSo the history stays current without re-reading everything

About the change history and the update notification, stated precisely because they are the two places other data passes through the app. Jira's changelog endpoint returns all recorded changes for an issue, which can include edits to fields the app does not care about: a summary being rewritten, a description being edited. The update notification Jira sends when an issue changes carries the same kind of thing, plus whatever fields Jira chooses to put on the issue in that payload. In both cases those entries arrive in the app's memory as part of the message. The app reads only the status-change entries out of them and stores nothing else. Nothing from any other field is written to storage, and nothing is sent anywhere. The app never requests issue summaries, descriptions, comments, attachments or custom fields as fields.


3. Exactly what is stored, table by table

This is the complete list. There are no other tables.

The app is also told, by Atlassian, with each request, whether this site holds an active subscription to it. That answer is read and acted on; it is not written to any table above and it is not sent anywhere.

TableEvery column it holds
intervals, one row per status perioda row id; issue key; project key; status name; entered-at and exited-at timestamps; duration in ms; a sequence number; which write path produced it; assignee display name; issue type; a bucket number for that duration; the same duration counted inside your working calendar; a bucket number for that; and a flag recording whether the period was entered outside your working hours. The last three are empty until you save a working-hours calendar: the app derives no working-hours figure against a calendar you have not given it
issues, one row per issueissue key; project key; issue type; assignee display name; current status; created-at; last-updated-at
issue_cycle, one row per issueissue key; project; assignee display name; issue type; created-at; completed-at; number of passes through the workflow; eight cycle and lead-time figures, being three definitions of cycle time and one of lead time, each also counted inside your working calendar, with a bucket number for each of the eight; the time the issue spent in the statuses you group as blocked, also counted inside your working calendar, with a bucket number for each; a fingerprint of the cycle time definition the figures were computed under, which is the statuses you mark as in progress, as done, and as blocked; a fingerprint of the working calendar the working-hours figures were computed under; and when the row was last written
evt_log, the update ledgera row id; issue key; the Jira changelog entry id; when the transition happened; when it was stored; the difference between the two; and which of the timestamp formats Jira sent the transition time in. Rows older than thirty days are deleted
cycle_state, one rowa key naming the row; the cycle time definition the rebuild is running under, as a fingerprint; one issue key used as a cursor; how many issues are done; a status word; and two timestamps. No issue data beyond that one key
bf_state, one rowa key naming the row; the history build's cursor and the list of projects in scope, each as a short piece of text; counters (issues done, rows written, pages, invocations, issues whose history could not be read, requests throttled, time spent waiting, requests shed); two timestamps; a status word; a stop flag; and a note field this version never fills
settings, a handful of rowsa key, a value and a timestamp per row. The values are your working-hours calendar and a fingerprint of it; your status-group definitions; your cycle time definition, which is the statuses that count as in progress and as done; timing counters from the last history build; and a random secret, generated once for your installation, that is used to derive your saved-view key
view_state, one row per user per viewa one-way hash of your Atlassian account ID, and never the account ID itself; which view it is; the view itself (chosen project, grouping, JQL text, the date range chosen in the date pickers, and whether the status-groups, working-hours and cycle-time toggles were on); and a timestamp
scope_meta / scope_setan id for the scope; a JQL string you typed; when it was built; how many issues it matched; a status word; and, in the second table, that id paired with each issue key it matched. Cached so the same scope is not re-fetched
warm_loga row id; when the ping ran; an id and an age for the container that ran it; how long its query took; and which kind of ping it was. No issue data, no user data. Only the most recent 4,032 rows are kept; older ones are deleted

One value is kept outside those tables. It lives in the app's own key-value storage, which Atlassian provides for your installation alongside the database above, under the key sojourn:lastServedAt. It is a single number: the moment this installation last showed somebody a report. It holds no issue data, no user data and nothing about who asked. It is written at most once every five minutes, and it exists so that the app's scheduled background work can tell an installation that is in use from one nobody has opened, and stop running for the second. Nothing else is kept there.

Who can see what

The report covers only projects you can browse in Jira. Per-issue lists show only issues you can browse. Aggregate figures within a project you can browse may include issues that an issue security level hides from you; per-issue lists do not.

A per-issue surface - the list behind a "Now" count, the list behind a distribution band, one issue's history, and the section drawn in the issue's own column - additionally checks every issue it is about to show against your own Jira permissions, one by one, each time it is drawn. It reads only the issue key to do so, and stores nothing from that check.

To report a security issue, or to read how an incident would be handled, see the security page at https://lichenapps.github.io/sojourn/security/

The app BUILDS its history reading Jira as itself, because a background build has no user to run as; what it SHOWS you is checked against your own Jira permissions, as you, on every load, with no caching, so a permission change takes effect the next time you open a report. The page states how many projects were included. It never states, names or counts what was left out - Jira does not confirm that a project you cannot browse exists, and this app does not do what Jira refuses to do.

Personal data, named

Sojourn measures issues, not people. There is no built-in per-person breakdown: no report, gadget, in-issue section or export is broken down by person, and no figure the app computes is attributed to anyone.

Stated precisely, because the difference matters to whoever has to sign this off. A JQL scope you type yourself is Jira's own query language and your own words, so it can name a person exactly as it can in Jira, and the export's provenance block records the scope that produced the file, name and all: a file that misreported its own scope would be a worse problem than the one this rule exists to avoid. What the app never does is compute anything per person.

One of the columns above is personal data. A second is derived from personal data and is called out beside it, because the distinction is the sort a reader should be able to check rather than take on trust:

  1. Assignee display name. Stored on every interval and every issue row. It is used for one thing: answering a JQL scope you type yourself, such as assignee = "...", from the app's own store instead of asking Jira for it. It is never a row, a column, a grouping or a chart, and it is in no export. Earlier versions could group a report by assignee; that was removed on 2026-09-09 and the sentence above is what replaced it. It is the display name only. It is never the account ID. An earlier version of this app stored account IDs by accident in one of its two write paths, which was found, fixed, and is now prevented by a check that runs against the stored data itself.
  2. The key your saved view is stored under. The app does not store your Atlassian account ID. view_state is keyed by a value derived from it with a one-way hash, under a random secret that is generated once for your installation, kept in that installation's own storage, and never sent anywhere. The stored value is a hash and not an encrypted copy, so there is no key that turns it back, and it is meaningless outside your own installation because the secret it was derived under never leaves it. It is a key: never displayed, and not joined to anything else. A request that carries no account at all saves no view, rather than being given a fallback key it would share with every other such request.

The app stores no Atlassian account IDs, no email addresses, no avatars, no names other than the assignee display name above, and no free text written by a person other than a JQL query they typed themselves.

Who can see and change what

The app is installed once for the whole site, and the tables above are the site's, not any one user's. Three things follow, and they are stated here rather than left to be discovered:

  1. Reports are not the same for everyone. The app reads Jira as itself when it BUILDS the history, because a background build has no user to run as. What it SHOWS you is restricted to the projects you can browse in Jira, checked as you on every load, so two people looking at the same scope see different numbers. There are no per-person figures to see either: there is no built-in per-person breakdown anywhere in the app. The one residual is stated under "Who can see what" above and is not restated here.
  2. Settings are instance-wide, and only a Jira administrator can change them. The working-hours calendar, the cycle time definition and the status groups belong to the installation. Changing any of them marks the stored working-hours or cycle figures as computed under a definition that no longer applies, and the app then refuses to show them until they are rebuilt - so the blast radius of one edit is every figure on the site, for everybody. The app therefore asks Jira, as you, whether you hold the ADMINISTER permission before it writes any of them, and refuses if you do not or if that question cannot be answered. There is no separate administrator role inside the app: it is your Jira one.
  3. Only the view is per user. view_state is the one table with a row per person, keyed by the derived value described above, and it holds the view you last chose and nothing else.

If that is broader than your site wants, the control is who you grant access to the app.


4. Where it lives, and for how long


5. What the app does NOT do

6. Permissions, and why each is held

Every permission the app holds over your Jira data is read-only. There are no write scopes on Jira and no egress permissions. One scope in the table, storage:app, is not a Jira permission: it is the app's own Atlassian-hosted storage, and it gives the app no access to anything in Jira. Every scope below is held for a call the app actually makes; the calls are the complete list, not a sample.

ScopeThe call that needs it
storage:appThe app's own key-value storage, held by Atlassian for your installation. It is used for exactly one value - the time this installation last served a report, described at the end of section 3 - so the app's scheduled background work can stop running on an installation nobody is using. It reads nothing from Jira and writes nothing to Jira
read:jira-workClassic scope required by Jira's search and changelog endpoints through the Forge proxy. The granular scopes below are not sufficient on their own: without this one, every search returns 401
read:jira-userThe other half of that pair, and the assignee display name that comes back on a search result
read:jql:jiraPOST /rest/api/3/search/jql and POST /rest/api/3/search/approximate-count. The JQL you type in the report's scope box is parsed and counted by Jira before any of it is fetched
read:issue.changelog:jiraPOST /rest/api/3/changelog/bulkfetch. This is the app's whole raw material: the status-change history
read:issue-meta:jiraThe created date on each issue, which is where every duration starts
read:issue:jiraReading the issues a search matched at all, and GET /rest/api/3/issue/{key} - one issue, by key, asked for the same six fields listed above and no others. It is used for two things, both because Jira's search index is a few seconds behind reality: reconciliation tells an issue that MOVED from one that was deleted, so stored history is never deleted because a search was briefly out of date; and a newly created issue is read directly, because the index does not yet know an issue that is one second old
read:jira-userGET /rest/api/3/mypermissions, AS YOU, asking one question: whether you hold Jira's ADMINISTER permission. It is asked once, immediately before the app writes a setting that belongs to the whole site - the working calendar, the cycle time definition, the status groups - and it is refused if the answer is no or if the question cannot be answered. The answer is used and not stored
read:project:jiraGET /rest/api/3/project/search, the list of projects, and the project field on each issue. A whole-instance query is rejected by Jira, so work is partitioned by project. The same endpoint is called AS YOU, with action=browse, on every report load, to establish which projects you may browse; the answer is used and not stored
read:status:jiraThe status field, and the status names inside the change history
read:field:jiraResolving the six field names the app asks for by name in every request
read:issue-type:jiraThe issuetype field, which reports group by
read:user:jiraThe assignee's display name, the one piece of personal data the app stores

Removed permissions

Holding a permission that is never used is a claim a customer cannot check, so two that this app does not use were removed on 2026-08-28 rather than left in place:

Neither is requested any more, and this is the whole of what was removed. The app was redeployed and re-installed without them, and its full pipeline (the history build, the incremental update path, the JQL scope builder and the report) was then exercised against real Jira data, because a missing permission is enforced by Atlassian at request time and cannot be caught by any test that does not make the request.

7. Sub-processors

None. There are no sub-processors, because there is no processing anywhere except inside your own Atlassian instance.

8. Security

The app holds no credentials of its own. It runs on Atlassian Forge, authenticates as an app through Atlassian's own mechanism, and stores data only through Atlassian's provisioned storage. The vendor operates no servers, holds no keys, and has no path to your data.

9. Changes to this policy

If a future version of the app ever needed to send data anywhere, it would need declared egress in its manifest, would lose the Runs on Atlassian badge, and would require you to approve the new permission before it could be installed. That is not a promise; it is how the platform works. This policy would be updated before any such version was submitted.

10. Contact

support@lichenapps.com