Skip to content

Add TeamDynamix v1 for Education ITSM - #110

Open
shawn149 wants to merge 1 commit into
mainfrom
work/sw/team-dynamix
Open

Add TeamDynamix v1 for Education ITSM#110
shawn149 wants to merge 1 commit into
mainfrom
work/sw/team-dynamix

Conversation

@shawn149

@shawn149 shawn149 commented Aug 7, 2026

Copy link
Copy Markdown

Introducing the TeamDynamix plugin (v1)

🔌 Plugin overview

  • Plugin name: TeamDynamix
  • Purpose / problem solved: Brings TeamDynamix ITSM data into SquaredUp so service desk performance, asset inventory and the CMDB can sit on the same dashboards as everything else. Every useful TeamDynamix endpoint is scoped to a numeric application ID, so the plugin imports applications first and derives the object type from the application's type. A user picks "Service Desk" by name from an object picker and never sees an app ID.
  • Primary audience (e.g. platform teams, SREs, product teams): IT service desk and ITSM teams, IT asset managers, and anyone reporting on ticket throughput or SLA attainment
  • Authentication method(s) (e.g. OAuth, Username/Password, API Key): API key. A BEID and Web Services Key pair is exchanged for a 24 hour JWT via POST /api/auth/loginadmin. No built-in authMode covers a token exchange, so this runs in a pre-request script that caches the token against its own exp claim.

🖼️ Plugin screenshots

⚠️ I have not been able to capture these yet. The plugin has never run against a real TeamDynamix instance, so there is no authenticated data source to screenshot. A customer has agreed to test it, and I will add the configuration and dashboard screenshots once they have it connected.

Plugin configuration

pending customer test

Default dashboards

pending customer test


🧪 Testing

I have no TeamDynamix instance and no credentials, so nothing here has been tested end to end against a live back end. A customer has agreed to be the first test. I have tried to make that first run as informative as possible rather than pretend the gap isn't there.

Unit tests. Since I could not test streams against real data, I wrote offline checks.


⚠️ Known limitations

Nothing has been tested against a live TeamDynamix instance.
Five assumptions can only be settled by a real tenant:

  • Whether loginadmin returns the token as raw text or as a JSON-quoted string. The script strips optional surrounding quotes so it handles both, but only one has ever been observed.
  • Whether the sandbox Web API path really is /SBTDWebApi. This is inferred, not confirmed, and the field help says so.
  • Whether OrgApplication.Type returns exactly Ticketing and Assets/CI on a real tenant. These values are documented in the spec and they are what split applications into the three object types, so if a tenant reports something else the object pickers will be wrong.
  • Whether the gremlin scope.query on sourceType resolves as expected for the dependent asset and configuration item import steps. Both are marked optional, so a miss degrades to a warning instead of failing the whole import.
  • Whether a real CMDB fits under the response size limit.

The API has no pagination, so results are capped. This is a genuine TeamDynamix constraint. The /search endpoints return a single unpaged result set with only a MaxResults cap, and paged access exists only through saved searches. ConfigurationItemSearch has no cap parameter at all, which makes the Configuration Items tile the most likely thing to fail on a large estate. Defaults are 2000 records, configurable per tile, and this is documented in the README.

Tickets are not imported into the graph. They are high-volume transactional records rather than infrastructure, so they are queried live through the Tickets data stream instead. People are not imported either, for volume and PII reasons.

Wide records are trimmed. A TeamDynamix ticket has 119 fields including the full HTML description plus attachment and task arrays. Passing those through overruns the response size cap at a few hundred rows, so the scripts project to a dashboard-usable column set. Ticket descriptions, attachments and task detail are therefore not available. Custom attributes are flattened into attr.-prefixed columns, hidden by default.

Time entries identify people by UID. The time search returns a person's unique identifier and no name field, so time cannot be grouped by person name.

Follow-ups. Report Builder (/api/reports) is not implemented, since it depends on reports a customer may not have. Attachments, feeds, ticket tasks, and project plans, issues and risks are all out of scope for v1. Once the customer confirms the five assumptions we might revisit whether projects, services and knowledge base articles should be imported as objects rather than left as application-scoped streams.


📚 Checklist

  • Plugin, datastream and UI naming follow SquaredUp guidelines
  • Logo added
  • One or more dashboards added
  • README added including configuration guidance
  • No secrets or credentials included
  • I agree to the Code of Conduct

Summary by CodeRabbit

  • New Features
    • Added TeamDynamix integration with secure configuration, authentication, and connection validation.
    • Added monitoring for tickets, assets, configuration items, services, projects, locations, time entries, knowledge-base articles, and related records.
    • Added Overview, Service Desk, and Assets & CMDB dashboards with filters, charts, tables, and key metrics.
    • Added configurable scopes, record limits, status filters, date ranges, and billable options.
  • Documentation
    • Added setup guidance, supported data, permissions, limitations, and read-only access details.

Introduce TeamDynamix v1 plugin: adds metadata, UI, config validation, index definitions, icons and README. Adds comprehensive dataStreams (tickets, assets, configurationItems, projects, services, KB articles, timeEntries, locations, accounts, groups, applications, ticketStatuses, currentUser) plus post-request scripts to project/flatten results. Implements preRequest.js for BEID/WebServicesKey authentication, token caching, request spacing and rate-limit handling, and a shared error-handling script. Includes default dashboards, scopes, and custom type definitions for out-of-the-box monitoring.
@shawn149
shawn149 requested a review from a team August 7, 2026 21:16
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a TeamDynamix v1 plugin with authenticated API access, configuration validation, discovery and operational data streams, indexed objects, custom types, default scopes, dashboards, and setup documentation.

Changes

TeamDynamix integration

Layer / File(s) Summary
Plugin contract and authentication
plugins/TeamDynamix/v1/metadata.json, plugins/TeamDynamix/v1/ui.json, plugins/TeamDynamix/v1/preRequest.js, plugins/TeamDynamix/v1/configValidation.json, plugins/TeamDynamix/v1/custom_types.json, plugins/TeamDynamix/v1/docs/README.md
Defines plugin metadata, credential fields, validation steps, custom types, request throttling, JWT authentication, token caching, and setup documentation.
Discovery streams and indexed objects
plugins/TeamDynamix/v1/dataStreams/{currentUser,applications,accounts,groups,locations}.json, plugins/TeamDynamix/v1/dataStreams/scripts/{applications,accounts,locations}.js, plugins/TeamDynamix/v1/dataStreams/scripts/errorHandling/rateLimit.js, plugins/TeamDynamix/v1/indexDefinitions/default.json
Adds hidden discovery streams, normalized projections, shared HTTP error handling, and default indexed objects.
Ticketing and work-management streams
plugins/TeamDynamix/v1/dataStreams/{tickets,ticketStatuses,services,projects,knowledgeBaseArticles,timeEntries}.json, plugins/TeamDynamix/v1/dataStreams/scripts/{tickets,services,projects,knowledgeBaseArticles}.js
Adds configurable searches and normalized records for tickets, statuses, services, projects, knowledge-base articles, and time entries.
Asset and CMDB streams
plugins/TeamDynamix/v1/dataStreams/{assets,configurationItems}.json, plugins/TeamDynamix/v1/dataStreams/scripts/{assets,configurationItems}.js
Adds application-scoped asset and configuration-item searches with state filters, metadata mappings, custom attributes, and normalized links.
Scopes and default dashboards
plugins/TeamDynamix/v1/defaultContent/{scopes.json,manifest.json,overviewDashboard.dash.json,serviceDeskDashboard.dash.json,assetsDashboard.dash.json}
Adds application scope variables, dashboard registration, and default dashboards for overview, service desk, and assets/CMDB data.

Sequence Diagram(s)

sequenceDiagram
  participant Dashboard
  participant DataStream
  participant preRequest
  participant TeamDynamixAPI
  Dashboard->>DataStream: request filtered records
  DataStream->>preRequest: submit API request
  preRequest->>TeamDynamixAPI: authenticate and send request
  TeamDynamixAPI-->>DataStream: return API response
  DataStream-->>Dashboard: return normalized records
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the addition of TeamDynamix v1 support for Education ITSM.
Description check ✅ Passed The description explains the plugin purpose, authentication, scope, testing status, limitations, screenshots, and checklist.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

🧩 Plugin PR Summary

📦 Modified Plugins

  • plugins/TeamDynamix/v1

📋 Results

Step Status
Validation ❌ Failed
Deployment ⏭️ Skipped

🔍 Validation Details

plugins/TeamDynamix/v1
{
  "valid": false,
  "errors": [
    {
      "file": "dataStreams/tickets.json",
      "message": "Invalid value — expected one of: none, dashboard",
      "path": [
        "defaultTimeframe"
      ]
    },
    {
      "file": "dataStreams/timeEntries.json",
      "message": "Invalid value — expected one of: none, dashboard",
      "path": [
        "defaultTimeframe"
      ]
    }
  ]
}

@shawn149 shawn149 added the new-plugin Used to PR newly added plugins label Aug 7, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plugins/TeamDynamix/v1/dataStreams/configurationItems.json`:
- Around line 9-13: Update the configurationItems stream around endpointPath,
postBody, and paging so it applies a supported server-side CMDB response bound
before enabling the stream. Do not add an unsupported MaxResults field; use an
API-supported filter or disable the stream for tenants that cannot guarantee a
safe response size.

In `@plugins/TeamDynamix/v1/dataStreams/projects.json`:
- Around line 79-81: Remove decimalPlaces and thousandsSeparator from the
numeric metadata shapes in plugins/TeamDynamix/v1/dataStreams/projects.json
lines 79-81, leaving the budget fields as raw number shapes; apply the same
change to the hours, bill rate, and cost rate fields in
plugins/TeamDynamix/v1/dataStreams/timeEntries.json lines 48-60. Configure
presentation formatting only in the corresponding dashboard columns.

In `@plugins/TeamDynamix/v1/dataStreams/scripts/configurationItems.js`:
- Around line 9-13: Update the configuration item transformation that uses
BACKING_ITEM_TYPE_NAMES to preserve the upstream numeric BackingItemType,
emitting item.BackingItemType ?? null instead of a mapped label or empty string.
Change the corresponding configurationItems.json field definition to shape
"number", leaving presentation labels to dashboard formatting.

In `@plugins/TeamDynamix/v1/dataStreams/scripts/services.js`:
- Around line 4-11: Update
plugins/TeamDynamix/v1/dataStreams/scripts/services.js lines 4-11 and
plugins/TeamDynamix/v1/dataStreams/scripts/projects.js lines 6-13 so
customAttributeColumns receives records containing Attributes from a supported
detail endpoint before generating attr.* projections; alternatively remove the
attr.* projections and related metadata in both files. Ensure the selected
approach applies consistently to both service and project streams.

In `@plugins/TeamDynamix/v1/dataStreams/scripts/tickets.js`:
- Around line 35-43: Update the webBaseUrl normalization and ticketLink
construction to capture whether the source URL uses the sandbox TDWebApi prefix,
then reuse that prefix when selecting the deep-link application path. Production
URLs must continue using /TDNext, while sandbox URLs use /SBTDNext instead of
losing the sandbox segment.
- Around line 55-95: Update the ticket mapping flow to verify that data is an
array before calling .map. For non-array response bodies, use the existing
empty-result behavior instead of attempting to map the object, while preserving
normal mapping for valid array responses.

In `@plugins/TeamDynamix/v1/dataStreams/tickets.json`:
- Around line 126-129: Update the TeamDynamix stream configuration’s
defaultTimeframe value from last30days to dashboard, preserving the existing
timeframes and supportsNoneTimeframe settings so the dashboard timeframe
controls the actual window.

In `@plugins/TeamDynamix/v1/dataStreams/ticketStatuses.json`:
- Line 26: Update the StatusClass field declaration in the ticket status schema
to use shape "number", matching the integer enum returned by the unscripted
response passthrough. Do not add a projection or change the field’s visibility.

In `@plugins/TeamDynamix/v1/dataStreams/timeEntries.json`:
- Around line 66-68: Update the timeEntries timeframe configuration to replace
the unsupported defaultTimeframe value "last30days" with the supported value
"dashboard" or "none", while preserving the existing timeframes and
supportsNoneTimeframe settings.

In `@plugins/TeamDynamix/v1/docs/README.md`:
- Line 23: Update the “Ignore certificate errors” description in the
documentation table to state that it should be enabled only when the API
endpoint presents a self-signed or otherwise untrusted certificate, and remove
the implication that self-hosted instances inherently require it.

In `@plugins/TeamDynamix/v1/indexDefinitions/default.json`:
- Around line 21-22: Remove the “TeamDynamix ” prefix from source-type
identifiers such as the value in the default index definition, and update all
consumers in custom_types.json, metadata.json, index mappings, scopes, stream
matches, and producing scripts to use the upstream names. Preserve prefixed
custom-type values only where they serve as friendly display names. Create a new
major-version directory while retaining v1 unchanged, and update the new
metadata version accordingly.

In `@plugins/TeamDynamix/v1/ui.json`:
- Around line 15-18: Update the apiBaseUrl pattern validation to reject URLs
containing query (?) or fragment (#) components while continuing to accept valid
HTTPS host and path values. Preserve the existing full-address validation and
message behavior in the pattern configuration.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2c76d4d2-7dbe-4bdb-aa1a-e3506d63eb14

📥 Commits

Reviewing files that changed from the base of the PR and between b99d31f and ea54289.

⛔ Files ignored due to path filters (2)
  • plugins/TeamDynamix/v1/icon.png is excluded by !**/*.png
  • plugins/TeamDynamix/v1/icon.svg is excluded by !**/*.svg
📒 Files selected for processing (35)
  • plugins/TeamDynamix/v1/configValidation.json
  • plugins/TeamDynamix/v1/custom_types.json
  • plugins/TeamDynamix/v1/dataStreams/accounts.json
  • plugins/TeamDynamix/v1/dataStreams/applications.json
  • plugins/TeamDynamix/v1/dataStreams/assets.json
  • plugins/TeamDynamix/v1/dataStreams/configurationItems.json
  • plugins/TeamDynamix/v1/dataStreams/currentUser.json
  • plugins/TeamDynamix/v1/dataStreams/groups.json
  • plugins/TeamDynamix/v1/dataStreams/knowledgeBaseArticles.json
  • plugins/TeamDynamix/v1/dataStreams/locations.json
  • plugins/TeamDynamix/v1/dataStreams/projects.json
  • plugins/TeamDynamix/v1/dataStreams/scripts/accounts.js
  • plugins/TeamDynamix/v1/dataStreams/scripts/applications.js
  • plugins/TeamDynamix/v1/dataStreams/scripts/assets.js
  • plugins/TeamDynamix/v1/dataStreams/scripts/configurationItems.js
  • plugins/TeamDynamix/v1/dataStreams/scripts/errorHandling/rateLimit.js
  • plugins/TeamDynamix/v1/dataStreams/scripts/knowledgeBaseArticles.js
  • plugins/TeamDynamix/v1/dataStreams/scripts/locations.js
  • plugins/TeamDynamix/v1/dataStreams/scripts/projects.js
  • plugins/TeamDynamix/v1/dataStreams/scripts/services.js
  • plugins/TeamDynamix/v1/dataStreams/scripts/tickets.js
  • plugins/TeamDynamix/v1/dataStreams/services.json
  • plugins/TeamDynamix/v1/dataStreams/ticketStatuses.json
  • plugins/TeamDynamix/v1/dataStreams/tickets.json
  • plugins/TeamDynamix/v1/dataStreams/timeEntries.json
  • plugins/TeamDynamix/v1/defaultContent/assetsDashboard.dash.json
  • plugins/TeamDynamix/v1/defaultContent/manifest.json
  • plugins/TeamDynamix/v1/defaultContent/overviewDashboard.dash.json
  • plugins/TeamDynamix/v1/defaultContent/scopes.json
  • plugins/TeamDynamix/v1/defaultContent/serviceDeskDashboard.dash.json
  • plugins/TeamDynamix/v1/docs/README.md
  • plugins/TeamDynamix/v1/indexDefinitions/default.json
  • plugins/TeamDynamix/v1/metadata.json
  • plugins/TeamDynamix/v1/preRequest.js
  • plugins/TeamDynamix/v1/ui.json

Comment on lines +9 to +13
"endpointPath": "api/{{object.rawId}}/cmdb/search",
"postBody": {
"IsActive": "{{typeof activeState === 'undefined' || activeState === '' || activeState === 'all' ? null : activeState === 'active'}}"
},
"paging": { "mode": "none" },

@coderabbitai coderabbitai Bot Aug 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
file=$(git ls-files | grep -F 'plugins/TeamDynamix/v1/dataStreams/configurationItems.json' | head -n1)
printf '%s\n' "$file"
wc -l "$file"
cat -n "$file"
printf '\nRelated TeamDynamix files:\n'
git ls-files | grep -E '(^|/)plugins/TeamDynamix/|ConfigurationItemSearch|configurationItems' | head -80
printf '\nRelevant references:\n'
rg -n -i --glob '!plugins/TeamDynamix/v1/dataStreams/configurationItems.json' 'cmdb/search|ConfigurationItemSearch|objectLimit|paging.*none|IsActive' plugins README.md .github 2>/dev/null | head -160

Repository: squaredup/plugins

Length of output: 13086


🌐 Web query:

TeamDynamix CMDB API api/{id}/cmdb/search ConfigurationItemSearch result limit paging IsActive

💡 Result:

The TeamDynamix CMDB API provides two primary ways to search for Configuration Items (CIs). The method used depends on whether you are executing a custom search or using a pre-configured saved search. 1. Custom Search (POST api/{appId}/cmdb/search) The endpoint POST api/{appId}/cmdb/search accepts a TeamDynamix.Api.Cmdb.ConfigurationItemSearch object in the request body [1][2]. This method retrieves a list of CIs based on specified parameters but does not return full CI information (such as attributes or attachments) [1][3]. If you require detailed information, you must load individual items separately [1][4]. 2. Saved Search Results (POST api/{appId}/cmdb/searches/{searchId}/results) If you are working with pre-defined saved searches, you can use the endpoint POST api/{appId}/cmdb/searches/{searchId}/results [5][6]. This method allows for pagination, returning a TeamDynamix.Api.ResultPage<TeamDynamix.Api.Cmdb.ConfigurationItem> [5]. Regarding your specific questions: Paging and Result Limits: For the standard POST /cmdb/search method, the API documentation does not explicitly detail standard paging or result limit parameters, suggesting it may return a list directly [1][2]. However, the saved search results endpoint (POST /cmdb/searches/{searchId}/results) is explicitly designed for pagination [5][6]. IsActive Property: The ConfigurationItem object includes an IsActive property (Boolean), which indicates the active status of the CI and defaults to true [7][8]. While the search request body (ConfigurationItemSearch) allows you to filter results, if you need to filter by status explicitly, similar patterns in the TeamDynamix API (such as those used for Product Models) typically support a nullable Boolean IsActive filter (true for active, false for inactive, null for both) [9]. You should verify the specific members available within the TeamDynamix.Api.Cmdb.ConfigurationItemSearch object for your specific API version to confirm the exact parameter name for filtering by status. All CMDB API methods are rate-limited, typically restricted to 60 calls per IP address every 60 seconds [1][3][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' 'configurationItems.js:'
cat -n plugins/TeamDynamix/v1/dataStreams/scripts/configurationItems.js
printf '\nTeamDynamix metadata:\n'
cat -n plugins/TeamDynamix/v1/metadata.json
printf '\nSibling stream implementations:\n'
cat -n plugins/TeamDynamix/v1/dataStreams/services.json
cat -n plugins/TeamDynamix/v1/dataStreams/assets.json
printf '\nRepository response-limit and paging documentation:\n'
rg -n -i 'response limit|result limit|maximum.*rows|objectLimit|paging.*mode|baseDataSourceName|postRequestScript' --glob '*.md' --glob '*.json' --glob '*.js' | head -240
printf '\nWorking-tree summary:\n'
git status --short
git diff --stat

Repository: squaredup/plugins

Length of output: 13921


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from urllib.request import Request, urlopen
from html import unescape
import re

urls = [
    "https://solutions.teamdynamix.com/TDWebApi/Home/type/TeamDynamix.Api.Cmdb.ConfigurationItemSearch",
    "https://solutions.teamdynamix.com/TDWebApi/Home/section/ConfigurationItems",
]
for url in urls:
    print(f"\nURL: {url}")
    try:
        req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
        text = unescape(urlopen(req, timeout=15).read().decode("utf-8", "replace"))
        text = re.sub(r"<script\b[^>]*>.*?</script>", " ", text, flags=re.I|re.S)
        text = re.sub(r"<style\b[^>]*>.*?</style>", " ", text, flags=re.I|re.S)
        text = re.sub(r"<[^>]+>", " ", text)
        text = re.sub(r"\s+", " ", text)
        for term in ("ConfigurationItemSearch", "MaxResults", "Page", "IsActive", "Limit", "SearchText"):
            hits = [m.start() for m in re.finditer(term, text, flags=re.I)]
            print(term, len(hits))
            for pos in hits[:3]:
                print(" ", text[max(0,pos-180):pos+300])
    except Exception as exc:
        print(type(exc).__name__, exc)
PY

Repository: squaredup/plugins

Length of output: 628


🏁 Script executed:

#!/bin/bash
set -eu
for url in \
  'https://solutions.teamdynamix.com/TDWebApi/Home/type/TeamDynamix.Api.Cmdb.ConfigurationItemSearch' \
  'https://solutions.teamdynamix.com/TDWebApi/Home/section/ConfigurationItems'
do
  echo "URL: $url"
  curl -k -L --max-time 20 -A 'Mozilla/5.0' -sS "$url" |
    tr '\n' ' ' |
    sed -E 's/<script[^>]*>.*<\/script>/ /Ig; s/<style[^>]*>.*<\/style>/ /Ig; s/<[^>]+>/ /g; s/[[:space:]]+/ /g' |
    grep -Eo '.{0,220}(ConfigurationItemSearch|MaxResults|IsActive|PageSize|Page|Limit|SearchText).{0,320}' -i |
    head -30 || true
  echo
done

Repository: squaredup/plugins

Length of output: 340


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' 'All TeamDynamix CMDB stream and script references:'
rg -n -C 3 'ConfigurationItemSearch|cmdb/search|MaxResults|~6MB|response cap|response limit' plugins/TeamDynamix
printf '\nPotential framework limits and stream config schema:\n'
rg -n -i -C 2 '6.?MB|response.{0,20}cap|maximum.{0,20}(response|rows|records)|maxRecords|dataSources.*maxRecords|paging' . --glob '!**/*.png' --glob '!**/*.svg' | head -260

Repository: squaredup/plugins

Length of output: 27591


Add a supported response bound before enabling this stream.

POST /api/{appId}/cmdb/search has no MaxResults field, and paging is disabled. A large CMDB can exceed the stream response limit before configurationItems.js projects the rows. Add a server-side filter that guarantees a safe response size, or disable the stream for tenants without one.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/TeamDynamix/v1/dataStreams/configurationItems.json` around lines 9 -
13, Update the configurationItems stream around endpointPath, postBody, and
paging so it applies a supported server-side CMDB response bound before enabling
the stream. Do not add an unsupported MaxResults field; use an API-supported
filter or disable the stream for tenants that cannot guarantee a safe response
size.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the API support paging?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🤖 web_search executed:

TeamDynamix API documentation POST /api/{appId}/cmdb/search paging ConfigurationItemSearch POST /api/{appId}/cmdb/searches/{searchId}/results

Length of output: 6448


@clarkd, not for the endpoint in this stream.

POST /api/{appId}/cmdb/search returns a ConfigurationItem[]. The documented API does not provide pagination options for this endpoint.

TeamDynamix supports paging for saved CMDB searches:

POST /api/{appId}/cmdb/searches/{searchId}/results

That endpoint accepts pagination options and returns ResultPage<ConfigurationItem>. It requires a pre-existing saved search ID.

So, the current paging: { "mode": "none" } is correct for cmdb/search, but the unbounded-response concern remains.

You are interacting with an AI system.

Comment thread plugins/TeamDynamix/v1/dataStreams/projects.json
Comment thread plugins/TeamDynamix/v1/dataStreams/scripts/configurationItems.js
Comment on lines +4 to +11
const customAttributeColumns = (service) => {
const columns = {};
for (const attribute of service.Attributes || []) {
if (!attribute || !attribute.Name) continue;
columns[`attr.${attribute.Name}`] =
attribute.ValueText ?? (attribute.Value === "" ? null : attribute.Value ?? null);
}
return columns;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

: "${TD_API_BASE_URL:?Set the TeamDynamix API base URL}"
: "${TD_JWT:?Set a JWT with access to the test application}"
: "${TD_APP_ID:?Set an application ID}"

for endpoint in services/search projects/search; do
  curl -fsS \
    -H "Authorization: Bearer ${TD_JWT}" \
    -H "Content-Type: application/json" \
    -X POST \
    -d '{}' \
    "${TD_API_BASE_URL%/}/api/${TD_APP_ID}/${endpoint}" |
    jq '[.[] | has("Attributes")] | any'
done

Repository: squaredup/plugins

Length of output: 219


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate files ---'
git ls-files 'plugins/TeamDynamix/v1/dataStreams/scripts/services.js' \
  'plugins/TeamDynamix/v1/dataStreams/scripts/projects.js' \
  'plugins/TeamDynamix/v1/dataStreams/*.json' \
  'plugins/TeamDynamix/**/metadata.json'

printf '%s\n' '--- service script ---'
cat -n plugins/TeamDynamix/v1/dataStreams/scripts/services.js

printf '%s\n' '--- project script ---'
cat -n plugins/TeamDynamix/v1/dataStreams/scripts/projects.js

printf '%s\n' '--- endpoint and attribute references ---'
rg -n -C 3 'services/search|projects/search|Attributes|customAttributeColumns|attr\\.' plugins/TeamDynamix/v1

Repository: squaredup/plugins

Length of output: 20088


🌐 Web query:

TeamDynamix Web API ServiceCatalog services/search Attributes excluded response

💡 Result:

When using the TeamDynamix Web API endpoint POST /api/{appId}/services/search, the service returns a list of services, but it does not include full service information [1][2][3]. Specifically, certain properties are intentionally excluded from the response to optimize performance [1][2]. The "Attributes" property is one of these excluded fields [1][2][3]. In addition to Attributes, other commonly excluded properties in this response include: - Attachments [1][2][3] - LongDescription [1][2][3] - ServiceOfferings [1][2][3] Because these fields are not included in the search results, you must perform a separate API call to retrieve them. To access the full details (including attributes) for a specific service, you must load that service individually by its ID [1][2][3].

Citations:


🌐 Web query:

TeamDynamix Web API projects/search Attributes excluded response

💡 Result:

When using the TeamDynamix Web API, the POST /api/projects/search endpoint is designed to return a list of projects but does not provide the full project information [1][2][3]. Specifically, the following properties are excluded from the response of this search operation: - Attributes [1][4][5] - CustomColumns [1][2][6] - NonWorkingDays [1][3][5] Because these properties are excluded from the search results, you must load the project individually using a separate GET request (typically via the GET /api/projects/{id} endpoint) if you need to access that detailed information, including attributes [1][2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- stream configurations ---'
cat -n plugins/TeamDynamix/v1/dataStreams/services.json | sed -n '1,65p'
cat -n plugins/TeamDynamix/v1/dataStreams/projects.json | sed -n '1,100p'

printf '%s\n' '--- plugin metadata ---'
cat -n plugins/TeamDynamix/v1/metadata.json | sed -n '1,80p'

printf '%s\n' '--- version changes in the current worktree ---'
git diff --stat -- plugins/TeamDynamix
git diff -- plugins/TeamDynamix/v1/metadata.json

Repository: squaredup/plugins

Length of output: 11397


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import json
import re

cases = {
    "plugins/TeamDynamix/v1/dataStreams/scripts/services.js": (
        "plugins/TeamDynamix/v1/dataStreams/services.json",
        "service",
        "services/search",
    ),
    "plugins/TeamDynamix/v1/dataStreams/scripts/projects.js": (
        "plugins/TeamDynamix/v1/dataStreams/projects.json",
        "project",
        "projects/search",
    ),
)

for script_name, (stream_name, item_name, endpoint_suffix) in cases.items():
    script = Path(script_name).read_text()
    stream = json.loads(Path(stream_name).read_text())
    endpoint = stream["config"]["endpointPath"]

    has_attribute_loop = re.search(
        rf"for\s*\(\s*const\s+attribute\s+of\s+{item_name}\.Attributes\s*\|\|\s*\[\]\s*\)",
        script,
    ) is not None
    emits_prefixed_columns = "columns[`attr.${attribute.Name}`]" in script
    uses_search_endpoint = endpoint.endswith(endpoint_suffix)

    # Model the exact fallback used by the loop for a search result.
    search_result = {"ID": 1, "Name": "sample"}
    attributes = search_result.get("Attributes") or []
    produced_columns = {
        f"attr.{a['Name']}": a.get("ValueText") or a.get("Value")
        for a in attributes
        if a and a.get("Name")
    }

    print(json.dumps({
        "script": script_name,
        "endpoint": endpoint,
        "uses_search_endpoint": uses_search_endpoint,
        "reads_attributes": has_attribute_loop,
        "emits_attr_columns": emits_prefixed_columns,
        "columns_for_result_without_Attributes": produced_columns,
        "projection_is_empty_for_search_shape": produced_columns == {},
    }, sort_keys=True))
PY

Repository: squaredup/plugins

Length of output: 279


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import json
import re

cases = {
    "plugins/TeamDynamix/v1/dataStreams/scripts/services.js": (
        "plugins/TeamDynamix/v1/dataStreams/services.json",
        "service",
        "services/search",
    ),
    "plugins/TeamDynamix/v1/dataStreams/scripts/projects.js": (
        "plugins/TeamDynamix/v1/dataStreams/projects.json",
        "project",
        "projects/search",
    ),
}

for script_name, (stream_name, item_name, endpoint_suffix) in cases.items():
    script = Path(script_name).read_text()
    stream = json.loads(Path(stream_name).read_text())
    endpoint = stream["config"]["endpointPath"]

    has_attribute_loop = re.search(
        rf"for\s*\(\s*const\s+attribute\s+of\s+{item_name}\.Attributes\s*\|\|\s*\[\]\s*\)",
        script,
    ) is not None
    emits_prefixed_columns = "columns[`attr.${attribute.Name}`]" in script
    uses_search_endpoint = endpoint.endswith(endpoint_suffix)

    search_result = {"ID": 1, "Name": "sample"}
    attributes = search_result.get("Attributes") or []
    produced_columns = {
        f"attr.{a['Name']}": a.get("ValueText") or a.get("Value")
        for a in attributes
        if a and a.get("Name")
    }

    print(json.dumps({
        "script": script_name,
        "endpoint": endpoint,
        "uses_search_endpoint": uses_search_endpoint,
        "reads_attributes": has_attribute_loop,
        "emits_attr_columns": emits_prefixed_columns,
        "columns_for_result_without_Attributes": produced_columns,
        "projection_is_empty_for_search_shape": produced_columns == {},
    }, sort_keys=True))
PY

Repository: squaredup/plugins

Length of output: 747


Retrieve custom attributes from detail responses.

Both services/search and projects/search omit Attributes, so both attr.* projections remain empty. Fetch detail records from a supported source before running these projections, or remove the projections and metadata.

  • plugins/TeamDynamix/v1/dataStreams/scripts/services.js#L4-L11
  • plugins/TeamDynamix/v1/dataStreams/scripts/projects.js#L6-L13
📍 Affects 2 files
  • plugins/TeamDynamix/v1/dataStreams/scripts/services.js#L4-L11 (this comment)
  • plugins/TeamDynamix/v1/dataStreams/scripts/projects.js#L6-L13
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/TeamDynamix/v1/dataStreams/scripts/services.js` around lines 4 - 11,
Update plugins/TeamDynamix/v1/dataStreams/scripts/services.js lines 4-11 and
plugins/TeamDynamix/v1/dataStreams/scripts/projects.js lines 6-13 so
customAttributeColumns receives records containing Attributes from a supported
detail endpoint before generating attr.* projections; alternatively remove the
attr.* projections and related metadata in both files. Ensure the selected
approach applies consistently to both service and project streams.

Comment on lines +35 to +43
const webBaseUrl = String(context?.dataSources?.[0]?.baseUrl || "")
.trim()
.replace(/\/+$/, "")
.replace(/\/(SB)?TDWebApi$/i, "");

const ticketLink = (ticket) =>
webBaseUrl
? `${webBaseUrl}/TDNext/Apps/${ticket.AppID}/Tickets/TicketDet?TicketID=${ticket.ID}`
: ticket.Uri || "";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve the sandbox segment in the derived deep link.

Line 38 strips both /TDWebApi and /SBTDWebApi, but line 42 always rebuilds the link with /TDNext. For a sandbox base address the web UI path is /SBTDNext, so every sandbox link points at the production application. Capture the sandbox prefix and reuse it.

🔧 Proposed fix
-const webBaseUrl = String(context?.dataSources?.[0]?.baseUrl || "")
+const rawBaseUrl = String(context?.dataSources?.[0]?.baseUrl || "")
     .trim()
-    .replace(/\/+$/, "")
-    .replace(/\/(SB)?TDWebApi$/i, "");
+    .replace(/\/+$/, "");
+
+// A sandbox Web API address (/SBTDWebApi) has a matching sandbox UI at /SBTDNext.
+const isSandbox = /\/SBTDWebApi$/i.test(rawBaseUrl);
+const webBaseUrl = rawBaseUrl.replace(/\/(SB)?TDWebApi$/i, "");
+const nextApp = isSandbox ? "SBTDNext" : "TDNext";
 
 const ticketLink = (ticket) =>
     webBaseUrl
-        ? `${webBaseUrl}/TDNext/Apps/${ticket.AppID}/Tickets/TicketDet?TicketID=${ticket.ID}`
+        ? `${webBaseUrl}/${nextApp}/Apps/${ticket.AppID}/Tickets/TicketDet?TicketID=${ticket.ID}`
         : ticket.Uri || "";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/TeamDynamix/v1/dataStreams/scripts/tickets.js` around lines 35 - 43,
Update the webBaseUrl normalization and ticketLink construction to capture
whether the source URL uses the sandbox TDWebApi prefix, then reuse that prefix
when selecting the deep-link application path. Production URLs must continue
using /TDNext, while sandbox URLs use /SBTDNext instead of losing the sandbox
segment.

{ "name": "Order", "displayName": "Order", "shape": "number" },
{ "name": "IsActive", "displayName": "Active", "shape": "boolean" },
{ "name": "IsDefault", "displayName": "Default", "shape": "boolean" },
{ "name": "StatusClass", "displayName": "Status Class", "shape": "string", "visible": false }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Declare StatusClass as a number.

plugins/TeamDynamix/v1/dataStreams/scripts/tickets.js (lines 7-16) documents StatusClass as an integer enum on the wire. This stream passes the response through without a script, so the declared string shape does not match the value. Either declare "shape": "number", or add a projection that maps the enum to the class name for readability.

🔧 Proposed fix
-        { "name": "StatusClass", "displayName": "Status Class", "shape": "string", "visible": false }
+        { "name": "StatusClass", "displayName": "Status Class", "shape": "number", "visible": false }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/TeamDynamix/v1/dataStreams/ticketStatuses.json` at line 26, Update
the StatusClass field declaration in the ticket status schema to use shape
"number", matching the integer enum returned by the unscripted response
passthrough. Do not add a projection or change the field’s visibility.

Comment on lines +66 to +68
"timeframes": true,
"supportsNoneTimeframe": true,
"defaultTimeframe": "last30days"

@coderabbitai coderabbitai Bot Aug 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Use a supported defaultTimeframe value.

The validator rejects "last30days". This prevents plugin validation and deployment. Set defaultTimeframe to "dashboard" or "none".

Proposed fix
-    "defaultTimeframe": "last30days"
+    "defaultTimeframe": "dashboard"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/TeamDynamix/v1/dataStreams/timeEntries.json` around lines 66 - 68,
Update the timeEntries timeframe configuration to replace the unsupported
defaultTimeframe value "last30days" with the supported value "dashboard" or
"none", while preserving the existing timeframes and supportsNoneTimeframe
settings.

Source: Pipeline failures

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely needs fixing on all affected data streams. Currently blocking validation. Did you use the Claude build-plugin skill to generate this? Kind of surprised it generated something invalid.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- TeamDynamix defaultTimeframe values ---'
rg -n -C 3 '"defaultTimeframe"\s*:' plugins/TeamDynamix

echo
echo '--- Repository references to defaultTimeframe and last30days ---'
rg -n -C 2 '"defaultTimeframe"\s*:|"last30days"' \
  -g '*.json' \
  -g '*.js' \
  . \
  | head -n 500

echo
echo '--- Candidate schema/validator files ---'
fd -HI -t f '(schema|Schema|validator|Validator|dataStream|DataStream)' . \
  | head -n 300

Length of output: 28574


@clarkd, confirmed. The invalid value occurs in two data streams:

  • plugins/TeamDynamix/v1/dataStreams/timeEntries.json
  • plugins/TeamDynamix/v1/dataStreams/tickets.json

Use "defaultTimeframe": "dashboard" in both files. This preserves the "last30days" timeframe configured by the default dashboards. Do not change the dashboard timeframe values.

I cannot confirm whether the Claude build-plugin skill generated this PR. The PR content and repository evidence do not record the generation method.

  • Replace the unsupported stream defaultTimeframe value in both affected TeamDynamix streams.
  • Increase the TeamDynamix plugin version in metadata.json, as required for plugin changes.
  • Validate the edited JSON and confirm that no TeamDynamix stream still uses "defaultTimeframe": "last30days".

✏️ Learnings added
Learnt from: clarkd
Repo: squaredup/plugins PR: 110
File: plugins/TeamDynamix/v1/dataStreams/timeEntries.json:66-68
Timestamp: 2026-08-07T21:48:47.556Z
Learning: For the TeamDynamix v1 plugin, `defaultTimeframe` in data stream definitions only accepts supported stream-default values such as `"dashboard"` or `"none"`. The value `"last30days"` is valid in dashboard `timeframe` fields but blocks plugin validation when used as a data stream `defaultTimeframe`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


  • 📌 Create a pull request with these changes

You are interacting with an AI system.

| **BEID** | A GUID identifying your TeamDynamix organization. Exchanged with the Web Services Key for a session token. | TDAdmin → **Organization detail**. | Yes |
| **Web Services Key** | The secret paired with the BEID. Stored encrypted and never displayed again. | TDAdmin → **Organization detail**. | Yes |
| **Maximum records per request** | Caps how many records a single search returns. Defaults to 2000. | Advanced options — leave as-is unless you hit the limits described below. | No |
| **Ignore certificate errors** | Accepts self-signed certificates. Only needed for a self-hosted instance. | Advanced options. | No |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Limit certificate bypass to certificate trust failures.

Line 23 implies that self-hosted instances need this setting. A self-hosted endpoint can use a trusted certificate. Disabling certificate validation permits endpoint impersonation and machine-in-the-middle attacks. State that users must enable this setting only when the API endpoint presents a self-signed or otherwise untrusted certificate. (mas.owasp.org)

Proposed documentation change
-| **Ignore certificate errors** | Accepts self-signed certificates. Only needed for a self-hosted instance. | Advanced options. | No |
+| **Ignore certificate errors** | Accepts self-signed or otherwise untrusted certificates. Enable only when the Web API endpoint cannot present a trusted certificate. | Advanced options. | No |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/TeamDynamix/v1/docs/README.md` at line 23, Update the “Ignore
certificate errors” description in the documentation table to state that it
should be enabled only when the API endpoint presents a self-signed or otherwise
untrusted certificate, and remove the implication that self-hosted instances
inherently require it.

Comment thread plugins/TeamDynamix/v1/indexDefinitions/default.json
Comment on lines +15 to +18
"pattern": {
"value": "^https://[^\\s/]+(/[^\\s]*)?$",
"message": "Enter the full https address of your TeamDynamix Web API, e.g. https://yourorg.teamdynamix.com/TDWebApi"
}

@coderabbitai coderabbitai Bot Aug 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject query and fragment components in apiBaseUrl.

The pattern accepts URLs with ? or #. preRequest.js lines 51-53 then appends /api/auth/loginadmin as text, which puts that endpoint path inside the query or fragment. Authentication fails for an otherwise accepted configuration.

Proposed fix
-                "value": "^https://[^\\s/]+(/[^\\s]*)?$",
+                "value": "^https://[^\\s/@?#]+(?:/[^\\s?#]*)?$",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/TeamDynamix/v1/ui.json` around lines 15 - 18, Update the apiBaseUrl
pattern validation to reject URLs containing query (?) or fragment (#)
components while continuing to accept valid HTTPS host and path values. Preserve
the existing full-address validation and message behavior in the pattern
configuration.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a big issue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@clarkd, agreed. This is a minor validation edge case and is not blocking for this pull request.

You are interacting with an AI system.

"name": "TeamDynamix Account",
"sourceType": "TeamDynamix Account",
"icon": "building",
"singular": "Account / Department",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just use one name here?

Comment on lines +9 to +13
"endpointPath": "api/{{object.rawId}}/cmdb/search",
"postBody": {
"IsActive": "{{typeof activeState === 'undefined' || activeState === '' || activeState === 'all' ? null : activeState === 'active'}}"
},
"paging": { "mode": "none" },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the API support paging?

return headers[name] || headers[name.toLowerCase()] || headers[name.toUpperCase()];
};

if (response.status === 429) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will prevent the platform auto handling the rate limit.

Comment on lines +66 to +68
"timeframes": true,
"supportsNoneTimeframe": true,
"defaultTimeframe": "last30days"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely needs fixing on all affected data streams. Currently blocking validation. Did you use the Claude build-plugin skill to generate this? Kind of surprised it generated something invalid.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only a single icon is needed, SVG preferred.

"displayName": "TeamDynamix",
"version": "1.0.0",
"author": {
"name": "SquaredUp Labs",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vinbab to advise if best as labs or community.

const isTightlyLimited = /\/(tickets|time)\/search\/?$/i.test(url.pathname);
const minGapMs = isTightlyLimited ? 2100 : 1100;

const sinceLastRequest = Date.now() - (state?.lastRequestAt ?? 0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooft, this is neat but seems fairly involved. I'd rather leave all this to the platform.

@@ -0,0 +1,70 @@
[
{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would remove this - the tooltips and docs should serve the same purpose.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new-plugin Used to PR newly added plugins

Development

Successfully merging this pull request may close these issues.

2 participants