Add TeamDynamix v1 for Education ITSM - #110
Conversation
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.
📝 WalkthroughWalkthroughAdds 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. ChangesTeamDynamix integration
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
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Comment |
🧩 Plugin PR Summary📦 Modified Plugins
📋 Results
🔍 Validation Details❌
|
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (2)
plugins/TeamDynamix/v1/icon.pngis excluded by!**/*.pngplugins/TeamDynamix/v1/icon.svgis excluded by!**/*.svg
📒 Files selected for processing (35)
plugins/TeamDynamix/v1/configValidation.jsonplugins/TeamDynamix/v1/custom_types.jsonplugins/TeamDynamix/v1/dataStreams/accounts.jsonplugins/TeamDynamix/v1/dataStreams/applications.jsonplugins/TeamDynamix/v1/dataStreams/assets.jsonplugins/TeamDynamix/v1/dataStreams/configurationItems.jsonplugins/TeamDynamix/v1/dataStreams/currentUser.jsonplugins/TeamDynamix/v1/dataStreams/groups.jsonplugins/TeamDynamix/v1/dataStreams/knowledgeBaseArticles.jsonplugins/TeamDynamix/v1/dataStreams/locations.jsonplugins/TeamDynamix/v1/dataStreams/projects.jsonplugins/TeamDynamix/v1/dataStreams/scripts/accounts.jsplugins/TeamDynamix/v1/dataStreams/scripts/applications.jsplugins/TeamDynamix/v1/dataStreams/scripts/assets.jsplugins/TeamDynamix/v1/dataStreams/scripts/configurationItems.jsplugins/TeamDynamix/v1/dataStreams/scripts/errorHandling/rateLimit.jsplugins/TeamDynamix/v1/dataStreams/scripts/knowledgeBaseArticles.jsplugins/TeamDynamix/v1/dataStreams/scripts/locations.jsplugins/TeamDynamix/v1/dataStreams/scripts/projects.jsplugins/TeamDynamix/v1/dataStreams/scripts/services.jsplugins/TeamDynamix/v1/dataStreams/scripts/tickets.jsplugins/TeamDynamix/v1/dataStreams/services.jsonplugins/TeamDynamix/v1/dataStreams/ticketStatuses.jsonplugins/TeamDynamix/v1/dataStreams/tickets.jsonplugins/TeamDynamix/v1/dataStreams/timeEntries.jsonplugins/TeamDynamix/v1/defaultContent/assetsDashboard.dash.jsonplugins/TeamDynamix/v1/defaultContent/manifest.jsonplugins/TeamDynamix/v1/defaultContent/overviewDashboard.dash.jsonplugins/TeamDynamix/v1/defaultContent/scopes.jsonplugins/TeamDynamix/v1/defaultContent/serviceDeskDashboard.dash.jsonplugins/TeamDynamix/v1/docs/README.mdplugins/TeamDynamix/v1/indexDefinitions/default.jsonplugins/TeamDynamix/v1/metadata.jsonplugins/TeamDynamix/v1/preRequest.jsplugins/TeamDynamix/v1/ui.json
| "endpointPath": "api/{{object.rawId}}/cmdb/search", | ||
| "postBody": { | ||
| "IsActive": "{{typeof activeState === 'undefined' || activeState === '' || activeState === 'all' ? null : activeState === 'active'}}" | ||
| }, | ||
| "paging": { "mode": "none" }, |
There was a problem hiding this comment.
🩺 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 -160Repository: 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:
- 1: https://solutions.teamdynamix.com/TDWebApi/Home/section/ConfigurationItems
- 2: https://teamdynamix.umich.edu/TDWebApi/Home/section/ConfigurationItems
- 3: https://tdx.cornell.edu/TDWebApi/Home/section/ConfigurationItems
- 4: https://help.ohio.edu/TDWebApi/Home/section/ConfigurationItems
- 5: https://solutions.teamdynamix.com/TDWebApi/Home/section/ConfigurationItemReports
- 6: https://tdx.cornell.edu/TDWebApi/Home/section/ConfigurationItemReports
- 7: https://solutions.teamdynamix.com/TDWebApi/Home/type/TeamDynamix.Api.Cmdb.ConfigurationItem
- 8: https://tdx.cornell.edu/TDWebApi/Home/type/TeamDynamix.Api.Cmdb.ConfigurationItem
- 9: https://help.unlv.edu/TDWebApi/Home/member/TeamDynamix.Api.Assets.ProductModelSearch.IsActive
🏁 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 --statRepository: 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)
PYRepository: 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
doneRepository: 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 -260Repository: 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.
There was a problem hiding this comment.
🧩 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.
| 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; |
There was a problem hiding this comment.
🗄️ 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'
doneRepository: 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/v1Repository: 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:
- 1: https://support.gmhec.org/TDWebApi/Home/section/ServiceCatalog
- 2: https://aces.teamdynamix.com/TDWebApi/Home/section/ServiceCatalog
- 3: https://stfx.teamdynamix.com/TDWebApi/Home/section/ServiceCatalog
🌐 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:
- 1: https://aces.teamdynamix.com/TDWebApi/Home/section/Projects
- 2: https://tdx.cornell.edu/TDWebApi/Home/section/Projects
- 3: https://utk.teamdynamix.com/TDWebApi/Home/section/Projects
- 4: https://rollins.teamdynamix.com/TDWebApi/Home/section/Projects
- 5: https://ufl.teamdynamix.com/TDWebApi/Home/section/Projects
- 6: https://atlas.utdallas.edu/TDWebApi/Home/section/Projects
🏁 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.jsonRepository: 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))
PYRepository: 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))
PYRepository: 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-L11plugins/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.
| 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 || ""; |
There was a problem hiding this comment.
🎯 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 } |
There was a problem hiding this comment.
🗄️ 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.
| "timeframes": true, | ||
| "supportsNoneTimeframe": true, | ||
| "defaultTimeframe": "last30days" |
There was a problem hiding this comment.
🎯 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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
🧩 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 300Length of output: 28574
@clarkd, confirmed. The invalid value occurs in two data streams:
plugins/TeamDynamix/v1/dataStreams/timeEntries.jsonplugins/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
defaultTimeframevalue 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 | |
There was a problem hiding this comment.
🔒 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.
| "pattern": { | ||
| "value": "^https://[^\\s/]+(/[^\\s]*)?$", | ||
| "message": "Enter the full https address of your TeamDynamix Web API, e.g. https://yourorg.teamdynamix.com/TDWebApi" | ||
| } |
There was a problem hiding this comment.
🎯 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.
There was a problem hiding this comment.
@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", |
There was a problem hiding this comment.
Should we just use one name here?
| "endpointPath": "api/{{object.rawId}}/cmdb/search", | ||
| "postBody": { | ||
| "IsActive": "{{typeof activeState === 'undefined' || activeState === '' || activeState === 'all' ? null : activeState === 'active'}}" | ||
| }, | ||
| "paging": { "mode": "none" }, |
| return headers[name] || headers[name.toLowerCase()] || headers[name.toUpperCase()]; | ||
| }; | ||
|
|
||
| if (response.status === 429) { |
There was a problem hiding this comment.
I think this will prevent the platform auto handling the rate limit.
| "timeframes": true, | ||
| "supportsNoneTimeframe": true, | ||
| "defaultTimeframe": "last30days" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Only a single icon is needed, SVG preferred.
| "displayName": "TeamDynamix", | ||
| "version": "1.0.0", | ||
| "author": { | ||
| "name": "SquaredUp Labs", |
| const isTightlyLimited = /\/(tickets|time)\/search\/?$/i.test(url.pathname); | ||
| const minGapMs = isTightlyLimited ? 2100 : 1100; | ||
|
|
||
| const sinceLastRequest = Date.now() - (state?.lastRequestAt ?? 0); |
There was a problem hiding this comment.
Ooft, this is neat but seems fairly involved. I'd rather leave all this to the platform.
| @@ -0,0 +1,70 @@ | |||
| [ | |||
| { | |||
There was a problem hiding this comment.
I would remove this - the tooltips and docs should serve the same purpose.
Introducing the TeamDynamix plugin (v1)
🔌 Plugin overview
POST /api/auth/loginadmin. No built-inauthModecovers a token exchange, so this runs in a pre-request script that caches the token against its ownexpclaim.🖼️ Plugin screenshots
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.
Nothing has been tested against a live TeamDynamix instance.
Five assumptions can only be settled by a real tenant:
loginadminreturns 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./SBTDWebApi. This is inferred, not confirmed, and the field help says so.OrgApplication.Typereturns exactlyTicketingandAssets/CIon 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.scope.queryonsourceTyperesolves as expected for the dependent asset and configuration item import steps. Both are markedoptional, so a miss degrades to a warning instead of failing the whole import.The API has no pagination, so results are capped. This is a genuine TeamDynamix constraint. The
/searchendpoints return a single unpaged result set with only aMaxResultscap, and paged access exists only through saved searches.ConfigurationItemSearchhas 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
Summary by CodeRabbit