Get started

Team Reports plugin

Team Reports collects GitHub organization activity and optional Discord discussion into daily, weekly, and monthly reports. It keeps report history on the Gateway and adds a Reports tab to the Control UI. Reports include activity counts, per-person history, source warnings, and optional model-written summaries.

Team Reports is an official external package: it is not part of the core openclaw npm package and is installed on demand from ClawHub or npm. Source checkouts of the repository load it directly from extensions/team-reports. It stays disabled until you enable it. Report pages use Gateway authentication. They are not public just because their default path is /reports.

Before you begin

You need a GitHub token that can read your configured organizations, repositories, and team membership. Collection can only include data that the token can access. For Discord, use a bot token with access to the selected guild, channels, threads, and message history. Only explicitly configured channels and their threads count.

Fine-grained GitHub tokens are supported. Issue searches always specify is:issue or is:pull-request, with merge searches limited to pull requests.

Model-written summaries use an agent's configured model and credentials. The evidence sent to that model can include repository activity and opted-in Discord excerpts. Set summaries.enabled: false to generate reports with deterministic text and no summary model calls.

Install and enable Team Reports

Install the package unless you run the Gateway from a source checkout, which already contains it:

bash
openclaw plugins install @openclaw/team-reports

Then add the following to your OpenClaw configuration, replacing the example organization, team, and login with your own:

json5
{  plugins: {    entries: {      "team-reports": {        enabled: true,        config: {          github: {            token: {              source: "env",              provider: "default",              id: "TEAM_REPORTS_GITHUB_TOKEN",            },            orgs: ["example-org"],            teams: [{ org: "example-org", slug: "maintainers" }],          },          people: [{ github: ["example-member"], display: "Example Member" }],          summaries: { enabled: false },        },      },    },  },}

Make the referenced environment variable available to the Gateway process. See Secret management for other secret providers. If you use plugins.allow, include team-reports in that list.

Restart the Gateway after changing plugin configuration, then check startup:

bash
openclaw gateway restartopenclaw team-reports status --jsonopenclaw dashboard

On startup, yesterday triggers a catch-up run after 60 seconds unless a successful run started at or after that day's closing UTC midnight and includes that day. A completed manual run after close also satisfies catch-up, including one that finishes during the startup delay or deferred wait. A successful run that started while the day was still open does not satisfy closed-day catch-up. Status shows the run, stored periods, next scheduled times, and source warnings. To request a report immediately, use:

bash
openclaw team-reports generate --intraday

Generation returns a run ID before collection and summarization finish. Check status for the result, then open Reports in the Control UI.

Read reports in the Control UI

The Reports tab appears for an enabled plugin and a Control UI connection with operator.read. It embeds the report page in a sandboxed frame. The Gateway supplies and renews a scoped authentication cookie; no Gateway token is added to report URLs.

Every report route requires operator.read; operator.write and operator.admin also satisfy that requirement. The Control UI tab grants operator.read through its scoped cookie. The CLI's Gateway RPC methods already declare their required read or generation scopes.

Use HTTPS, Tailscale Serve, or a browser-trusted loopback origin. Plain HTTP on a LAN hostname cannot authenticate the frame. Browsers that block all third-party cookies can also make the tab unavailable.

GitHub and other external links may not open inside the sandboxed frame. Each page includes Open in a new window with that page's own URL. If the browser blocks that action too, copy the link into a new tab. Gateway authentication still applies there.

Pages render on the server, work without JavaScript, and follow the browser's light or dark color preference. The index shows recent reports and a 28-day activity trend; people pages show each member's recent daily history.

Configuration

All keys below live under plugins.entries.team-reports.config. Unknown keys are rejected. Configuration and secret changes require a Gateway restart; secrets resolve once when the report service starts.

Key Default Behavior
basePath "/reports" Absolute route root with nonempty path segments using letters, digits, ., _, and -. It must not use . or .. segments, the /api/channels prefix, or equal or sit below an explicitly configured Control UI base path. Trailing slashes are normalized away.
displayTimezone "UTC" IANA timezone for displayed timestamps. Report windows always use UTC.
github required GitHub collection configuration, described below.
discord unset Optional Discord collection configuration. Omit it to collect GitHub only.
people unset Inline identity entries. Mutually exclusive with peopleFile.
peopleFile unset Absolute path to a regular JSON file of at most 2 MiB, shaped as { "people": [...] } and using the identity fields below.
summaries defaults below Model selection and summary enablement.
schedule defaults below UTC collection times and aggregate refreshes.
retention.days 400 Remove stored reports older than this many days after closed-day runs; 0 keeps all history.

GitHub

Key Default Behavior
github.token required Nonempty token string or SecretRef { source, provider, id }. Secret sources are env, file, exec, and store. Prefer a SecretRef.
github.orgs required Nonempty array of organization names to collect.
github.teams [] Roster sources shaped as { org, slug }.
github.includeDirectCollaborators false Add direct repository collaborators with push, maintain, or admin access to the roster.
github.excludeRepos [] Repository names in owner/name form to skip. Archived repositories are also skipped.
github.apiBaseUrl "https://api.github.com" HTTPS API base URL; set this for GitHub Enterprise Server.
github.ignoreCommentPatterns [] Regular expression source strings. Matching issue comments and PR review comments are excluded. Invalid patterns are rejected.

Discord

The following is an optional partial configuration example. IDs are illustrative:

validate=false
discord: {  token: {    source: "env",    provider: "default",    id: "TEAM_REPORTS_DISCORD_TOKEN",  },  guildId: "123456789012345678",  channels: [    { id: "234567890123456789", excerpts: true },    { id: "345678901234567890" },  ],  excerptMaxChars: 260,},
Key Default Behavior
discord.token required when Discord is configured Bot token string or SecretRef with the same shape as github.token.
discord.guildId required when Discord is configured Guild ID.
discord.channels required when Discord is configured Nonempty array of channel entries, each with an id and optional excerpts. Their threads also count.
discord.channels[].excerpts false Include bounded message excerpts in reports and the summary evidence. Message counts still contribute when this is false.
discord.excerptMaxChars 260 Maximum characters in each excerpt; integer from 1 to 4000.

People and identity

The roster combines GitHub team members, active identity entries, and direct collaborators when enabled. Use an identity entry to join a person's GitHub aliases and Discord user ID:

validate=false
people: [  {    github: ["example-member", "example-member-old"],    display: "Example Member",    affiliation: "Example Company",    roleGroup: "core",    roleLabel: "Maintainer",    access: ["release"],    areas: ["documentation"],    discordUserId: "456789012345678901",    discordUsername: "example-member",    status: "active",  },],
Field Required Behavior
github yes Nonempty login array. The first login is the primary identity; remaining logins are aliases.
display no Display name; defaults to the primary login.
affiliation no Optional affiliation label.
roleGroup no Group label, such as core, volunteer, or readonly; custom strings are accepted.
roleLabel no Human-readable role label.
access no Free-form labels, such as release or moderation; defaults to an empty list. These labels do not grant permissions.
areas no Ownership areas; defaults to an empty list.
discordUserId no Discord identity used to attribute messages to this person.
discordUsername no Optional Discord username metadata.
status no active or archived; omitted entries are active. Archived people are excluded from current reports while stored history remains available.
archivedAt no Archive date in YYYY-MM-DD form.

Summaries

Key Default Behavior
summaries.enabled true Generate model-written overview, highlights, and per-person summaries. false uses deterministic fallback text.
summaries.model unset Requested provider/model reference; otherwise use the target agent's default model. Requires the host policy below to take effect.
summaries.reasoning unset Requested thinking level: off, minimal, low, medium, high, xhigh, adaptive, max, or ultra. The host normalizes it for the selected model.
summaries.agentId unset Agent whose model and credentials are used. Cross-agent selection is subject to the host's llm.allowAgentIdOverride policy.

To permit a configured summary model, set plugins.entries.team-reports.llm.allowModelOverride: true. The llm object is a sibling of config, not a field inside it:

validate=false
"team-reports": {  enabled: true,  llm: { allowModelOverride: true },  config: {    // Keep your github and identity configuration here.    summaries: {      enabled: true,      model: "openai/gpt-5.6-sol",      reasoning: "high",    },  },},

Without that opt-in, Team Reports uses the target agent's default model. Host model allowlists still apply. See the plugin LLM runtime for those policies.

Summaries are grounded in a bounded evidence digest: totals, leading repositories and channels, selected activity items, and opted-in excerpts. They describe observed activity rather than infer performance, employment, or private facts. Quiet members remain in the report with a low-activity note.

The plugin validates the model's JSON response and retries once to repair invalid output or missing members. If the retry fails, it keeps the collected report and shows deterministic summaries with a visible fallback banner. The output budget scales with roster size; failed model attempts include a bounded, credential-free reason in the report, Markdown export, latest-day status, and Gateway logs. Unchanged evidence reuses the stored summary instead of making another model call. Collection is stored before summarization, which may take several minutes.

Schedule

Key Default Behavior
schedule.closedDayUtc "00:05" Daily run time in HH:MM UTC, before jitter. Generates yesterday's closed report and today's partial report.
schedule.intradayEveryHours 4 Refresh today's report on aligned UTC hour boundaries; integer from 0 to 24. 0 disables intraday runs.
schedule.jitterMinutes 5 Maximum random delay added to scheduled runs; integer from 0 to 59.
schedule.weekly true Refresh the current ISO week aggregate after daily collection.
schedule.monthly true Refresh the current month aggregate after daily collection.

Only one run executes at a time. Scheduled work waits for an active run; manual generation is rejected while another run is active. Runs have a 45-minute deadline. Stopping the service cancels its timers and waits up to 30 seconds for active work before closing storage.

Understand report windows and counts

Daily windows are UTC midnight through the next UTC midnight, with an exclusive end. Today's report is marked partial. ISO weeks start on Monday and use keys such as 2026-W34; months use keys such as 2026-08. displayTimezone changes timestamp labels only, never which report receives an event.

Weekly and monthly reports sum stored daily reports. They do not recollect the same period from the APIs. Missing days produce source warnings; generate the missing daily reports before relying on an aggregate's completeness. Closed-day runs also finish the week or month containing yesterday when the UTC date crosses a period boundary.

GitHub merge activity is credited to the person who merged the PR. Commits credit the author and each mapped coauthor once per person. Identical comments by the same actor and comment kind count once within the window. Bot activity is excluded, and nonmember GitHub actors appear separately with counts only. Members with no activity still appear in the roster.

Pull-request review submissions, including approval and request-changes bodies, are not collected; inline review comments are included.

Discord totals include unmapped authors, but unmatched entries contain only the author ID and count, without message content. Excerpts come only from channels with excerpts: true, use collapsed whitespace, and keep the newest eight per person. A person is active when either their GitHub or Discord count is nonzero.

Reports keep at most 200 GitHub items and eight Discord excerpts per person, with at most 80 aggregate top items. Stored report JSON is capped at 2 MiB; item lists are truncated deterministically, keeping newest items first, and the report indicates truncation. Counts can therefore exceed displayed items.

CLI and exports

The CLI talks to the running Gateway and supports --json plus the standard Gateway client options. Reading needs operator.read; generation needs operator.admin.

bash
openclaw team-reports status --jsonopenclaw team-reports list --jsonopenclaw team-reports show day 2026-08-20openclaw team-reports show week 2026-W34 --markdownopenclaw team-reports generate --date 2026-08-20openclaw team-reports generate --intraday

With no date or --intraday, generation selects yesterday. --intraday selects today; --date accepts a past day or today. Today's report remains partial. Future dates, or combining --intraday with a past date, are rejected.

With the default basePath, authenticated readers can use:

Path Result
/reports/ Report index and recent activity trend.
/reports/latest/ Redirect to the latest closed daily report.
/reports/day/<key>/ Daily HTML report; replace day with week or month for aggregates.
/reports/day/<key>/report.md Markdown export; also available for weeks and months.
/reports/day/<key>/data.json Structured report; also available for weeks and months.
/reports/people/ Roster index.
/reports/people/<login>/ Per-person history and 28-day trend.
/reports/index.json Latest keys and stored-period index.
/reports/status Run status, schedule, and source warnings as JSON.

These routes accept only GET and HEAD and send Cache-Control: private, no-store. Use the CLI or authenticated Gateway method to generate reports; reading a report page does not trigger collection.

Reports and run records live in the plugin-owned database at <state-dir>/plugins/team-reports/team-reports.sqlite. The plugin closes it when disabled or restarted. Retention runs after closed-day generation; set retention.days: 0 to preserve all report history.

Troubleshooting

The Reports tab is missing or unavailable. Confirm the plugin is enabled, allowed by plugins.allow if present, and the Control UI session has operator.read. Restart the Gateway after config changes. For an unavailable frame, check HTTPS or trusted loopback access and third-party-cookie policy.

There are no reports yet. Run openclaw team-reports status --json. Startup catch-up waits 60 seconds, and collection or model calls may still be running. Use generate --intraday for today's partial report. /latest/ requires at least one closed daily report.

A source has warnings or reports look incomplete. Read the warnings in status and the report. Check GitHub token access, organization/team names, excluded repositories, and Discord bot access to each configured channel and its history. Rate limits can delay a run. Regenerate affected days once access or rate limits recover, then refresh aggregates. Changing a secret requires a Gateway restart.

Repository advisories are optional. An advisory request returning HTTP 403 or 404 counts toward advisoriesSkipped in the GitHub source stats without adding a warning or marking the day stale. Rate-limit responses still wait and retry; other advisory failures retain their warnings.

Discord collection includes active and archived threads, including forum and media posts. Private archives require MANAGE_THREADS and READ_MESSAGE_HISTORY; if Discord returns HTTP 403, collection falls back to private threads the bot has joined, using READ_MESSAGE_HISTORY. If both private-archive endpoints return HTTP 403, the channel counts toward privateArchivesSkipped in Discord source stats without a warning or stale status; other failures retain their warnings. Private threads the bot cannot access are outside the report's coverage.

Long runs emit team-reports: progress lines in the Gateway log when roster, repository, issue-search, commit, comment/advisory, and Discord collection stages finish. These lines contain counts and the commit strategy, without tokens or message content.

A member is missing or Discord activity is unmatched. Check the GitHub team roster and identity entries. Put aliases in the same github array, use the person's Discord user ID, and ensure the entry is not archived. discordUsername alone does not map messages to a person.

Summaries show a fallback banner or ignore the requested model. Check the target agent's authentication and default model, summary settings, and host LLM policy. A requested model needs llm.allowModelOverride: true outside config. Fallback text preserves the report when the model is disabled, unavailable, or returns invalid output.

Generation says a run is already active. Inspect status and wait for that run's outcome. The scheduler prevents overlapping collection, and each run is bounded by its deadline.

See also: plugin configuration, Control UI, and Secret management.

Was this useful?
On this page

On this page