Compute Cycle Time from Linear CSV Exports in Python for Quarterly Engineering Reviews
This article shows a compact, production-ready pattern to compute cycle time from Linear issue CSV exports using a single-file Python script. It targets agency engineering managers and developer-tools teams that run quarterlies across multiple client accounts and need reliable, auditable metrics (median cycle time, 90th percentile, throughput) with minimal dependencies.
We’ll process CSVs that include timestamps (created_at, started_at, completed_at), project or client labels, and issue metadata, produce per-client summary statistics and CSV/JSON outputs, and explain how to run this as a repeatable Functory function or a local script.
What this function expects and what it produces
Input: a Linear CSV export file (UTF-8) with columns similar to:
- id (string) — Linear issue ID, e.g., 'LNR-1243'
- title (string)
- created_at (ISO 8601 string) — e.g., '2025-01-03T09:12:45Z'
- started_at (ISO 8601 string or empty)
- completed_at (ISO 8601 string or empty)
- project (string) — used as client account, e.g., 'acme-web'
- labels (string) — comma-separated labels like 'client:acme,priority:high'
Transformations performed:
- Parse timestamps to timezone-aware pandas datetime objects.
- Compute cycle_time = completed_at - started_at (fallback: created_at if started_at is null).
- Drop or mark issues without completed_at as in-progress (WIP) and optionally compute age as of report date.
- Group by project (client) and quarter to compute metrics: count, median, mean, p90, std, throughput (completed issues / quarter).
Output: JSON and CSV summary files containing per-client/quarter metrics and a filtered CSV of computed cycle times, e.g.:
[{"project":"acme-web","quarter":"2025-Q1","count":82,"median_days":3.5,"p90_days":12.2,"throughput_per_month":27.3}, ...]
Real-world scenario
Imagine an agency that manages 6 client products. You have a CSV export with 1,200 issues across 4 quarters, with fields: id,title,created_at,started_at,completed_at,project,assignee. Quarterly reviews require a table showing median cycle time and 90th percentile per client for Q1 2025 and a CSV to attach to the review slide deck.
Example concrete input (3 rows):
id,title,created_at,started_at,completed_at,project
LNR-1001,Fix login,2025-01-02T08:00:00Z,2025-01-02T10:00:00Z,2025-01-05T16:20:00Z,acme-web
LNR-1002,Update docs,2025-01-03T09:00:00Z,,2025-01-10T12:00:00Z,acme-web
LNR-1003,Payment bug,2025-01-07T11:30:00Z,2025-01-07T12:00:00Z,2025-01-07T14:00:00Z,globex-app
Corresponding outputs for acme-web Q1 2025 might be:
project,quarter,count,median_days,p90_days,throughput_per_month
acme-web,2025-Q1,82,3.5,12.2,27.3
Example dataset and the specific problem
Fabricated dataset: 1,200 rows, 6 projects (client accounts), 4 quarters (2024-Q4 to 2025-Q3). 70% of rows have started_at populated, 85% have completed_at populated; typical issue sizes range 0.5–20 days. The problem: prepare a reproducible quarterly report showing median cycle time and p90 by client, and surface outliers (issues > p90).
This script solves that by creating: (a) per-client quarterly aggregates, (b) CSV of outliers, (c) machine-readable JSON for dashboards.
Step-by-step mini workflow
- Download Linear CSV export: issues_2025_Q1.csv
- Run the Python script: python compute_cycle_time.py --input issues_2025_Q1.csv --output-dir reports/q1
- Script parses timestamps, computes cycle_time_days, filters completed issues, groups by project and quarter, writes reports/q1/summary_q1_2025.csv and outliers_q1_2025.csv
- Attach summary_q1_2025.csv to the quarterly review deck and import JSON into a BI dashboard for charts
Algorithm (high-level)
- Read CSV to DataFrame and parse created_at, started_at, completed_at to UTC datetimes.
- Compute start_time = started_at if present else created_at; compute cycle_time = completed_at - start_time.
- Filter rows with non-null completed_at; compute cycle_time_days = cycle_time.dt.total_seconds() / 86400.
- Assign quarter = f"{year}-Q{((month-1)//3)+1}" for completed_at.
- Group by project and quarter; aggregate count, mean, median, quantile(0.9), std; compute throughput = count / months_in_quarter.
- Export aggregate CSV and JSON; mark issues with cycle_time_days > p90 as outliers.
Concrete Python implementation (single-file)
The example below uses pandas only. Pin pandas in production (e.g., pandas==2.1.0).
from pathlib import Path
import pandas as pd
import argparse
def compute_cycle_time(df: pd.DataFrame) -> pd.DataFrame:
# parse timestamps
for c in ['created_at', 'started_at', 'completed_at']:
if c in df.columns:
df[c] = pd.to_datetime(df[c], utc=True, errors='coerce')
# determine effective start
df['start_time'] = df['started_at'].fillna(df['created_at'])
# compute cycle in days
df['cycle_time_days'] = (df['completed_at'] - df['start_time']).dt.total_seconds() / 86400
# only completed issues
df_done = df[df['completed_at'].notna()].copy()
# quarter label based on completed_at
df_done['quarter'] = df_done['completed_at'].dt.to_period('Q').astype(str)
return df_done
def summarize(df_done: pd.DataFrame) -> pd.DataFrame:
def p90(x):
return x.quantile(0.9)
grp = df_done.groupby(['project', 'quarter'])['cycle_time_days']
summary = grp.agg(count='count', mean='mean', median='median', p90=p90, std='std').reset_index()
# throughput per month (quarter is 3 months)
summary['throughput_per_month'] = summary['count'] / 3.0
# round numeric columns
for c in ['mean', 'median', 'p90', 'std', 'throughput_per_month']:
summary[c] = summary[c].round(2)
return summary
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--input', required=True)
parser.add_argument('--output-dir', default='reports')
args = parser.parse_args()
outdir = Path(args.output_dir)
outdir.mkdir(parents=True, exist_ok=True)
df = pd.read_csv(args.input)
df_done = compute_cycle_time(df)
summary = summarize(df_done)
summary.to_csv(outdir / 'summary.csv', index=False)
df_done.to_csv(outdir / 'issues_with_cycle_time.csv', index=False)
print('Wrote', outdir)
This script is intentionally small (one file) so it can be wrapped into a Functory function or scheduled in CI.
How Functory Makes It Easy
To publish this as a Functory function, wrap the core logic in a single main(...) entrypoint. On Functory you choose an exact Python patch version (for example 3.11.11) and create a requirements file with pinned versions such as:
pandas==2.1.0
The platform will call your main(input_file_path: str, output_dir: str) directly. Inputs become UI/API fields (uploaded CSV becomes a FilePath), and if main returns a path-like string the platform exposes the generated CSVs as downloadable artifacts.
Concretely a Functory main could accept: main(input_csv: str, quarter: str, project_filter: str = None) and return a path to reports/summary.csv. Functory handles the runtime (no servers), autoscaling, logs via print(), CPU/GPU tiers, and pay-per-use billing. You must declare all dependencies pinned to exact versions and the Python interpreter version; Functory bundles that environment and executes main(...) securely.
You can chain this function with others: e.g., run pre-processing function → compute-cycle-time function → chart-renderer function, each as separate Functory calls in a pipeline triggered by a webhook or an LLM agent.
Alternatives and why this approach is better
Common alternatives are manual spreadsheets, ad-hoc Jupyter notebooks, or built-in Linear analytics. Spreadsheets are error-prone at scale (copy/paste, timezone bugs), notebooks are hard to operationalize, and Linear’s native analytics may not expose the exact start/completed fallback logic or be easy to export per-client for slide decks. A single-file Python function gives repeatability, version control, and the ability to run as an API (Functory) for programmatic dashboards. It also lets you pin dependencies and build tests around the logic.
Business impact
Quantified benefit: automating cycle time computation for quarterly reviews can cut manual data prep from ~3 hours per account per quarter to ~20 minutes (≈88% reduction). For an agency with 6 accounts, that’s ~16 hours saved per quarter — time that can be reallocated to client work. Operationally, consistent metrics reduce argument time in reviews and help spotlight clients needing process improvements earlier.
Industry context
According to the 2023 State of DevOps / DORA reports, teams that measure and optimize lead time and cycle time are significantly more likely to achieve faster delivery and lower defect rates — a core reason why many organizations elevate cycle time into quarterly KPIs (Source: 2023 State of DevOps Report).
Comparison to other tooling
Manual scripts in bash + awk are brittle for ISO timestamps and timezones. Spreadsheets obfuscate transformations. BI tools may be overkill for small agencies and require ETL wiring. The function-based approach here hits a sweet spot: single-file, auditable, easy to run locally or publish to Functory as a hosted API, and suitable for automation and chaining.
Conclusion: A small, well-structured Python script reliably turns Linear CSV exports into repeatable cycle time metrics for quarterly engineering reviews. Next steps: add unit tests for timestamp edge cases, version and pin your dependencies (pandas==2.x.x), and publish the main(...) entrypoint on Functory so your PMs can run it on demand. Try it on one client CSV and iterate — publish your function for teammates once it’s stable.
Thanks for reading.
